Closed20

shadcn/uiでAvatarGroupを実装したい

ピン留めされたアイテム
hajimismhajimism

完成形!

import { FC } from "react";

import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/Avatar";

import { getInitial } from "./lib";

type Props = {
  avatarDataList: {
    image: string;
    name: string;
  }[];
  max?: number | undefined;
};

export const AvatarGroup: FC<Props> = ({ avatarDataList, max }) => {
  const avatarDataListWithinMax =
    max !== undefined ? avatarDataList.slice(0, max) : avatarDataList;
  const excess = max !== undefined ? avatarDataList.length - max : 0;

  // 子要素がrelativeのとき、flex-row-reverseでレイアウトすることで
  // z-indexを使わずに「先頭から順に下に重なっていく」を実現できるので
  // データ配列そのものも裏返すことで順序を元通りにする。
  const reversetDataList = [...avatarDataListWithinMax].reverse();

  return (
    <div className="flex space-x-reverse -space-x-2 flex-row-reverse justify-end">
      {excess > 0 && (
        <Avatar>
          <AvatarFallback> {`+${excess}`}</AvatarFallback>
        </Avatar>
      )}
      {reversetDataList.map((user, i) => (
        <Avatar key={i}>
          <AvatarImage src={user.image} alt={user.name} />
          <AvatarFallback>{getInitial(user.name)}</AvatarFallback>
        </Avatar>
      ))}
    </div>
  );
};

hajimismhajimism
hajimismhajimism

コメントはいってて読みやすいな

    const validChildren = getValidChildren(children)

    /**
     * get the avatars within the max
     */
    const childrenWithinMax =
      max != null ? validChildren.slice(0, max) : validChildren
  • childrenからAvatarを抽出
  • 最大数が設定されていればchildrenをそこまでに限定
hajimismhajimism

超過を+nで表示するために計算

    /**
     * get the remaining avatar count
     */
    const excess = max != null ? validChildren.length - max : 0

hajimismhajimism

テクいな...。基本的にchildrenの後ろのほうが上に積み重なるので、それを裏返す。


    /**
     * Reversing the children is a great way to avoid using zIndex
     * to overlap the avatars
     */
    const reversedChildren = childrenWithinMax.reverse()
hajimismhajimism

firstAvatarかどうかでスタイルを分けている

    const clones = reversedChildren.map((child, index) => {
      const isFirstAvatar = index === 0

      const childProps = {
        marginEnd: isFirstAvatar ? 0 : spacing,
        size: props.size,
        borderColor: child.props.borderColor ?? borderColor,
        showBorder: true,
      }

      return cloneElement(child, compact(childProps))
    })
hajimismhajimism

親のスタイル。justifyContent: "flex-end"flexDirection: "row-reverse"はなるほど?

    const groupStyles: SystemStyleObject = {
      display: "flex",
      alignItems: "center",
      justifyContent: "flex-end",
      flexDirection: "row-reverse",
      ...styles.group,
    }
hajimismhajimism

超過があれば表示

        {excess > 0 && (
          <chakra.span className="chakra-avatar__excess" __css={excessStyles}>
            {`+${excess}`}
          </chakra.span>
        )}
hajimismhajimism

getValidChildrenの中身はこれ。Chakra特有ではなく、childrenをarrayとして扱いたいときのutilっぽい。

import { Children, isValidElement } from "react"

/**
 * Gets only the valid children of a component,
 * and ignores any nullish or falsy child.
 *
 * @param children the children
 */
export function getValidChildren(children: React.ReactNode) {
  return Children.toArray(children).filter((child) =>
    isValidElement(child),
  ) as React.ReactElement[]
}
hajimismhajimism

こうやって使うらしいけど

<AvatarGroup size='md' max={2}>
  <Avatar name='Ryan Florence' src='https://bit.ly/ryan-florence' />
  <Avatar name='Segun Adebayo' src='https://bit.ly/sage-adebayo' />
  <Avatar name='Kent Dodds' src='https://bit.ly/kent-c-dodds' />
  <Avatar name='Prosper Otemuyiwa' src='https://bit.ly/prosper-baba' />
  <Avatar name='Christian Nwamba' src='https://bit.ly/code-beast' />
</AvatarGroup>

こういうinterfaceじゃだめなんか、という気持ちはちょっとある

<AvatarGroup users={users}>
hajimismhajimism

一旦大枠は完成したと思うんだけれど、ちらほらうまくいってない

import { FC } from "react";

import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/Avatar";

import { getInitial } from "./lib";

type Props = {
  avatarDataList: {
    image: string;
    name: string;
  }[];
  max?: number | undefined;
};

export const AvatarGroup: FC<Props> = ({ avatarDataList, max }) => {
  const avatarDataListWithinMax =
    max !== undefined ? avatarDataList.slice(0, max) : avatarDataList;
  const excess = max !== undefined ? avatarDataListWithinMax.length - max : 0;

  // flex-row-reverseでレイアウトすることでz-indexを使わずに「先頭から下に重なっていく」を実現できるので
  // データ配列そのものも裏返すことで順序を元通りにする。
  const reversetDataList = [...avatarDataListWithinMax].reverse();

  return (
    <div className="flex -space-x-2 flex-row-reverse justify-end">
      {reversetDataList.map((user, i) => (
        <Avatar key={i}>
          <AvatarImage src={user.image} alt={user.name} />
          <AvatarFallback>{getInitial(user.name)}</AvatarFallback>
        </Avatar>
      ))}
      {excess > 0 && (
        <Avatar>
          <AvatarImage src="" />
          <AvatarFallback> {`+${excess}`}</AvatarFallback>
        </Avatar>
      )}
    </div>
  );
};

hajimismhajimism
  • maxを超えているときもexcessが0になる
  • 最後のitemがなんか重なってない

hajimismhajimism

excessの計算間違っているのは確認した。修正版

  const excess = max !== undefined ? avatarDataList.length - max : 0;
hajimismhajimism

excess分を一番前に持ってこりゃ良いな、row-reverseのせいで直感的ではないが

      <div className="flex -space-x-2 flex-row-reverse justify-end">
        {excess > 0 && (
          <Avatar>
            <AvatarFallback> {`+${excess}`}</AvatarFallback>
          </Avatar>
        )}
        {reversetDataList.map((user, i) => (
          <Avatar key={i}>
            <AvatarImage src={user.image} alt={user.name} />
            <AvatarFallback>{getInitial(user.name)}</AvatarFallback>
          </Avatar>
        ))}
      </div>
hajimismhajimism

row-reverseを取り除くと最後のitemが重なってない問題が解決するのでここらへんにヒントがありそう

hajimismhajimism

わかった、これで解決した。

    <div className="flex space-x-reverse -space-x-4 flex-row-reverse justify-end">

-space-x-nが、「最初だけマージンつけない」をやってくれていて、reverseになってるからマージンつけない場所が逆転してたのが原因だった。

.-space-x-4 > :not([hidden]) ~ :not([hidden]) {
    --tw-space-x-reverse: 0;
    margin-right: calc(-1rem * var(--tw-space-x-reverse));
    margin-left: calc(-1rem * calc(1 - var(--tw-space-x-reverse)));
}
hajimismhajimism

-space-x-4だと+3の+が見えなかったので、-space-x-2にしておいた

このスクラップは2023/05/29にクローズされました