Icon komponenta

Martin a Kryštof řeší návrh komponenty pro SVG ikonky v Reactu. Shodují se, že by měla existovat jedna univerzální komponenta pro vlastní i externí ikonky (např. z @mdi/js), přičemž se do ní bude předávat přímo SVG path, ne jen název ikonky. Komponenta má být postavená tak, aby byla typově bezpečná, podporovala currentColor jako výchozí barvu, ale zároveň umožňovala předat otypované barvy jako prop.

Ohledně title se Martin a Kryštof baví o přístupnosti ikon. Martin navrhuje mít title jako povinný prop, aby nedošlo k opomenutí a každá ikona měla popisek. Kryštof ale připomíná, že podle specifikací (a jak to funguje i ve Vue) je absence title v pořádku, protože tím se ikona považuje za dekorativní a může být pro asistivní technologie ignorována.

Import ikon řeší kvůli optimalizaci (tree shaking). Martin varuje, že pokud se do komponenty Icon bude předávat jen název ikony (např. "mdiArrow"), je nutné importovat celý balík @mdi/js, čímž se do bundlu dostanou všechny ikonky. Řešením je místo toho předávat už konkrétní path, čímž se importuje jen potřebná ikona a zůstane funkční tree shaking. Kryštof souhlasí, že ideálně by komponenta Icon byla univerzální wrapper nad jakýmkoli SVG path, ať už vlastním nebo z balíčku.

1 Like

Ahojte,

zde nástřel advanced verze icon komponenty, která akceptuje string (path z ) nebo kompletní svg ikonky (React komponenty)

Icon component:

import { type VariantProps, cva } from 'class-variance-authority';
import { useId } from 'react';

type SvgIconProps = {
  title: string;
  color?: string;
} & React.SVGProps<SVGSVGElement>;

type SvgIcon = React.FunctionComponent<SvgIconProps>;

type IconTestProps = {
  icon: string | SvgIcon;
  title: string;
  color?: string;
} & VariantProps<typeof IconTestStyles>;

const IconTestStyles = cva('', {
  variants: {
    size: {
      small: 'ko:size-4',
      medium: 'ko:size-6',
      large: 'ko:size-8',
    },
  },
  defaultVariants: {
    size: 'medium',
  },
});
export function IconTest({ icon, size, title, color, ...props }: IconTestProps) {
  const titleId = `${title}-${useId()}`;

  if (typeof icon === 'string') {
    return (
      <svg xmlns="http://www.w3.org/2000/svg" aria-labelledby={titleId} aria-hidden={!title} role="img" className={IconTestStyles({ size })} viewBox="0 0 24 24" fill={color}>
        <title>{title}</title>
        <path d={icon} fill={color} />
      </svg>
    );
  }
  const SvgIcon = icon;
  return <SvgIcon title={title} aria-labelledby={titleId} aria-hidden={!title} fill={color} {...props} className={IconTestStyles({ size })} />;
}

Příklad SVG komponenty:

type EnvelopeIconProps = {
  title: string;
  color?: string;
} & React.JSX.IntrinsicAttributes &
  React.SVGProps<SVGSVGElement>;

export function EnvelopeIcon({ title, color, ...props }: EnvelopeIconProps) {
  return (
    <svg viewBox="0 0 24 24" fill={color} xmlns="http://www.w3.org/2000/svg" {...props} role="img">
      <title>{title}</title>
      <path d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z" fill={color} />
    </svg>
  );
}

Title jsem zatím nechal povinný kvůli biomu (noSvgWithoutTitle). Myslím, že v diskuzi jsme se nakonec shodli na verzi nepovinného titlu, pak upravím.

Co říkáte na zvolené accessibility props a s tím související použití hooku useId() ?

U te <SvgIcon> nemuzes pouzit fill={color} protoze v te ikonce muze byt cokoli (viz social ikonky) a kdyby si ji dal takhle fill, tak to bude mit nepredvidatelne nasledky. e.g. muze mit barevne pozadi.

Tady se fakt nabizi tu color vubec nemit a mit koncept currentColor vsude, kde to de. Tedy u path to mit jako fill a custom ikonku nechat tak, jak je. Pokud budeme vytvaret custom ikonky, tak muzou mit bud barvu vlastni (e.g. zase ty social ikonky), nebo taky pouzit currentColor.

Ok. Budeme tedy používat currentColor

Momentální setup komponenty umožňuje:

  1. Importovat ikonku z mdi/js (nebo jiné knihovny s path stringy) s default barvou nebo lze nastavit custom barvu s className

  2. Importovat SVG ikonku jako komponentu se svým vlastním setupem (e.g. custom barvy) + v případě custom monochromatické ikonky lze opět použít className přebarvit.

Může nastat případ, že se někdo bude pokoušet použít className na multipath ikonku, tím se dá zabránit použitím custom barev přímo v komponentě.

U singlepath ikonek se ponechá currentColor.

Předpokládám, že když se správný způsob použití zdokumentuje, neměl by být problém.

Super. Pouzit className na obe varianty urcite chceme. Ta custom ikonka v sobe muze/nemusi mit currentColor pripadne jine featury, co s tim funguji. Kazdopadne clovek, co to pouziva/implementuje si to musi pustit a vyzkouset. To uz je na vyvojari.

Hodis to do PR, prosim.

Chtěl bych ještě probrat jednu věc ohledně typování icon property:

Možné problémy:

a) string: Zaručit, že se jedná o validní path “svg-like” string ideálně z balíčku, které si nadefinujeme k používání (v tomto případě teď mdi/js)

b) SvgIcon: Vlastnostmi je SvgIcon kompatibilní s momentálním shapem našich custom ikon, hledám ještě optimálnější cestu pro vnucení používání výhradně z našeho adresáře.

Návrh řešení (zatím ve fázi zkoumání):

a) zvážit, zda by nebylo lepší používat icon prop jako otypované string keys (e.g. icon=“mdiAccount” nebo icon=“envelopeIcon”, s tím se pojí i b)

b) mergnout typy string a svgIcon v rámci např.: icon-registry.ts filu, který zajístí:

a) validní string a svgIcon typ
b) exportuje např.: Icon typ (string | svgIcon), který bude icon prop používat ve formě string keys
c) poslouží jako source of truth managementu ikonek v rámci tohoto projektu
d) zajisit, že řešení nebude ovlivňovat bundle size, budou se importovat pouze požadované ikony

Co si o tom myslíte, dává smysl o tom takto přemýšlet?

a) Neni potreba, je na kazdem vyvojari, ze ten string je validni. Proste musi si to pustit a kouknout. Pripadne napsat test, ze jeho komponenta nehazi do konzole renderovaci chybu.

b) Tohle formalne nejde. Abys dokazal vynutit pouzivani specifickeho setu, musel bys ten typ vyexportovat => vyexportovanim toho typu by se potom do klienta museli poslat uplne vsechny ikonky => nefungoval by tree shaking pri buildu.

Tohle je problem, co jsme resili tim typovanim, co si tam mel puvodne. Tim, ze sis vyexportoval typ jako union vsech exportu z nejakeho directory.

Me dava smysl reseni, co mame, ktere splnuje vsechno, co od toho chceme. Uz je potom na vyvojari, ze si da pozor, co tam laduje.

1 Like

Zde poslední verze (trochu vylepšené typy a clean-up propsů)

Icon component:

import { type VariantProps, cva } from 'class-variance-authority';
import { twMerge } from 'tailwind-merge';

type SvgIconProps = {
  title?: string;
} & React.SVGProps<SVGSVGElement>;

type SvgIcon = React.FunctionComponent<SvgIconProps>;

type BaseIconProps = {
  icon: string | SvgIcon;
} & VariantProps<typeof IconStyles> &
  React.SVGProps<SVGSVGElement>;

type ConditionalIconProps =
  | {
      title: string;
      decorative: false;
    }
  | { title?: string; decorative: true };

type IconProps = BaseIconProps & ConditionalIconProps;

const IconStyles = cva('', {
  variants: {
    size: {
      small: 'ko:size-4',
      medium: 'ko:size-6',
      large: 'ko:size-8',
    },
  },
  defaultVariants: {
    size: 'medium',
  },
});
export function Icon({ icon, size, title, decorative, className, ...props }: IconProps) {
  if (typeof icon === 'string') {
    return (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        aria-hidden={decorative ? 'true' : 'false'}
        focusable="false"
        role="img"
        className={twMerge(IconStyles({ size }), className)}
        viewBox="0 0 24 24"
        fill="currentColor"
      >
        {title && <title>{title}</title>}
        <path d={icon} fill="currentColor" />
      </svg>
    );
  }
  const SvgIcon = icon;
  return <SvgIcon title={title} focusable="false" role="img" aria-hidden={decorative ? 'true' : 'false'} {...props} className={twMerge(IconStyles({ size }), className)} />;
}

Custom icon:

type EnvelopeIconProps = {
  title?: string;
} & React.JSX.IntrinsicAttributes &
  React.SVGProps<SVGSVGElement>;

export function EnvelopeIcon({ title, ...props }: EnvelopeIconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
      {title && <title>{title}</title>}
      <path d="M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z" fill="currentColor" />
    </svg>
  );
}

Kvůli této line {title && <title>{title}</title>} se mi nepodařilo biome vyřešit jinak než změnou rule z "error" na "info", takto lint projde. Snad je to tak v pohodě.