Merged in feat/svg-instead-of-fonts (pull request #3411)

feat(SW-3695): use svg icons instead of font icons

* feat(icons): use svg instead of font icons

* feat(icons): use webpack/svgr for inlined svgs. Now support for isFilled again

* Merge master

* Remove old font icon


Approved-by: Joakim Jäderberg
This commit is contained in:
Linus Flood
2026-01-09 13:14:09 +00:00
parent faf1f17a11
commit cd59102ef4
47 changed files with 5357 additions and 3926 deletions

View File

@@ -29,8 +29,8 @@ import WindowNotAvailableIcon from "./Customised/Amenities_Facilities/WindowNotA
import WoodFloorIcon from "./Customised/Amenities_Facilities/WoodFloor"
import type { IconProps, NucleoIconProps } from "."
import type { MaterialSymbolProps } from "./MaterialIcon/MaterialSymbol"
import type { JSX } from "react"
import { MaterialIconName } from "./MaterialIcon/generated"
interface FacilityIconProps {
name: string | undefined
@@ -67,7 +67,7 @@ export function FacilityIcon({
}
const MaterialIconMappings: {
icon: MaterialSymbolProps["icon"]
icon: MaterialIconName
name: string
}[] = [
{ icon: "air_purifier_gen", name: "aircondition" },
@@ -112,7 +112,7 @@ const MaterialIconMappings: {
{ icon: "water_full", name: "complimentarycoldrefreshments" },
{ icon: "groups", name: "conventioncentre" },
{ icon: "accessible", name: "disabledparking" },
{ icon: "charging_station", name: "dockingstationforipodipad" },
{ icon: "mobile_charge", name: "dockingstationforipodipad" },
{ icon: "cool_to_dry", name: "dryingcabinet" },
{ icon: "assistant_navigation", name: "easyaccess" },
{ icon: "laundry", name: "garmentsteamer" },
@@ -172,7 +172,7 @@ const MaterialIconMappings: {
{ icon: "local_parking", name: "parking" },
{ icon: "local_parking", name: "parkingfreeparking" },
{ icon: "pets", name: "petfriendlyrooms" },
{ icon: "phone", name: "directdialphoneandvoicemail" },
{ icon: "phone_enabled", name: "directdialphoneandvoicemail" },
{ icon: "restaurant", name: "restaurant" },
{ icon: "room_service", name: "roomservice" },
{ icon: "sauna", name: "sauna" },

View File

@@ -11,7 +11,7 @@ import HairdryerIcon from "./Customised/Amenities_Facilities/Hairdryer"
import IceMachineIcon from "./Customised/Amenities_Facilities/IceMachine"
import InstagramIcon from "./Customised/Socials/Instagram"
import MassageIcon from "./Customised/Amenities_Facilities/Massage"
import { MaterialIcon, type MaterialIconSetIconProps } from "./MaterialIcon"
import PalmTreeIcon from "./Nucleo/Experiences/palm-tree-2"
import PopcornIcon from "./Nucleo/Food/popcorn-2"
import RecordPlayerIcon from "./Nucleo/Amenities_Facilities/record-player-3"
@@ -25,6 +25,7 @@ import { IconName } from "./iconName"
import type { IconProps, NucleoIconProps } from "./icon"
import type { JSX } from "react"
import { MaterialIcon, MaterialIconSetIconProps } from "./MaterialIcon"
interface IconByIconNameProps {
iconName: IconName | null
@@ -136,7 +137,7 @@ export function IconByIconName({
return <MaterialIcon icon="photo_camera" {...props} />
case IconName.Cellphone:
case IconName.Phone:
return <MaterialIcon icon="phone" {...props} />
return <MaterialIcon icon="phone_enabled" {...props} />
case IconName.HairdryerInRoomAllScandic:
return <HairdryerIcon {...props} />
case IconName.ComplimentaryColdRefreshments:

View File

@@ -1,40 +0,0 @@
import { MaterialSymbol, type MaterialSymbolProps } from "./MaterialSymbol"
import { iconVariants } from "../variants"
import type { VariantProps } from "class-variance-authority"
import { HTMLAttributes } from "react"
import { getIconAriaProps } from "../utils"
export interface MaterialIconProps
extends
Pick<MaterialSymbolProps, "size" | "icon" | "className" | "style">,
Omit<HTMLAttributes<HTMLSpanElement>, "color" | "id">,
VariantProps<typeof iconVariants> {
isFilled?: boolean
}
export type MaterialIconSetIconProps = Omit<MaterialIconProps, "icon">
export function MaterialIcon({
color,
size = 24,
className,
isFilled = false,
...props
}: MaterialIconProps) {
const classNames = iconVariants({ className, color })
const ariaProps = getIconAriaProps(props)
return (
// The span is used to prevent the MaterialSymbol from being underlined when used inside a link or button
<span>
<MaterialSymbol
{...props}
className={classNames}
data-testid="MaterialIcon"
size={size}
fill={isFilled}
{...ariaProps}
/>
</span>
)
}

View File

@@ -1,101 +0,0 @@
// This is adapted from https://github.com/edjonesdev/react-material-symbols
// since it doesn't support React 19 and is not maintained anymore.
// We should probably move to a different solution in the future.
import type { ElementType, CSSProperties, ReactElement, Ref } from "react"
import type {
MaterialSymbolWeight,
PolymorphicComponentProps,
SymbolCodepoints,
} from "./types"
import { cx } from "class-variance-authority"
export type { MaterialSymbolWeight, SymbolCodepoints } from "./types"
export type MaterialSymbolProps = {
/** Required. The name of the icon to render. */
icon: SymbolCodepoints
/** Default `false`.
*
* Fill gives you the ability to modify the default icon style. A single icon can render both unfilled and filled states. */
fill?: boolean
/** Weight defines the symbols stroke weight, with a range of weights between thin (100) and heavy (900). Weight can also affect the overall size of the symbol. */
weight?: MaterialSymbolWeight
/** Weight and grade affect a symbols thickness. Adjustments to grade are more granular than adjustments to weight and have a small impact on the size of the symbol. */
grade?: number
/** Default `'inherit'`.
*
* Size defines the icon width and height in pixels. For the image to look the same at different sizes, the stroke weight (thickness) changes as the icon size scales. */
size?: number
/** Default `'inherit'`
*
* Color accepts key values (`'red'`, `'blue'`, `'indigo'`, etc.), `<hex-color>`, `<rgb()>`, `<hsl()>` and `<hwb()>` values. */
color?: CSSProperties["color"]
className?: string
style?: CSSProperties
}
export type PolymorphicMaterialSymbolProps<C extends ElementType> =
PolymorphicComponentProps<C, MaterialSymbolProps>
export const MaterialSymbol = (<C extends ElementType>(
{
icon,
onClick,
as,
weight,
fill = false,
grade,
size,
style: propStyle,
color,
className,
...props
}: PolymorphicMaterialSymbolProps<C>,
ref: Ref<C>
): ReactElement => {
const Component =
onClick !== undefined ? "button" : ((as as ElementType) ?? "span")
const style = { color, ...propStyle }
if (fill)
style.fontVariationSettings = [style.fontVariationSettings, '"FILL" 1']
.filter(Boolean)
.join(", ")
if (weight)
style.fontVariationSettings = [
style.fontVariationSettings,
`"wght" ${weight}`,
]
.filter(Boolean)
.join(", ")
if (grade)
style.fontVariationSettings = [
style.fontVariationSettings,
`"GRAD" ${grade}`,
]
.filter(Boolean)
.join(", ")
if (size) {
style.fontVariationSettings = [
style.fontVariationSettings,
`"opsz" ${size}`,
]
.filter(Boolean)
.join(", ")
style.fontSize = size
}
return (
<Component
{...props}
ref={ref}
style={style}
onClick={onClick}
className={cx("material-symbols", className)}
>
{icon}
</Component>
)
}) as <C extends ElementType>(
props: PolymorphicMaterialSymbolProps<C>
) => ReactElement

View File

@@ -1 +0,0 @@
export { MaterialSymbol, type MaterialSymbolProps } from "./MaterialSymbol"

View File

@@ -1,55 +0,0 @@
import type {
JSXElementConstructor,
ComponentPropsWithoutRef,
JSX,
} from "react"
import type { MaterialSymbolWeightArray, SymbolCodepointsArray } from "./consts"
export type SymbolCodepoints = (typeof SymbolCodepointsArray)[number]
export type MaterialSymbolWeight = (typeof MaterialSymbolWeightArray)[number]
export interface AsProperty<C extends React.ElementType> {
/**
* An override of the default HTML tag.
* Can also be another React component.
*/
as?: C
}
/**
* A more sophisticated version of `InheritableElementProps` where
* the passed in `as` prop will determine which props can be included
*/
export type PolymorphicComponentProps<
C extends React.ElementType,
Properties = Record<string, unknown>,
> = InheritableElementProps<C, Properties & AsProperty<C>>
/**
* Source: https://github.com/emotion-js/emotion/blob/master/packages/styled-base/types/helper.d.ts
* A more precise version of just React.ComponentPropsWithoutRef on its own
*/
export type PropsOf<
C extends keyof JSX.IntrinsicElements | JSXElementConstructor<unknown>,
> = JSX.LibraryManagedAttributes<C, ComponentPropsWithoutRef<C>>
/**
* Allows for extending a set of props (`ExtendedProps`) by an overriding set of props
* (`OverrideProps`), ensuring that any duplicates are overridden by the overriding
* set of props.
*/
export type ExtendableProps<
ExtendedProperties = Record<string, unknown>,
OverrideProperties = Record<string, unknown>,
> = OverrideProperties & Omit<ExtendedProperties, keyof OverrideProperties>
/**
* Allows for inheriting the props from the specified element type so that
* props like children, className & style work, as well as element-specific
* attributes like aria roles. The component (`C`) must be passed in.
*/
export type InheritableElementProps<
C extends React.ElementType,
Properties = Record<string, unknown>,
> = ExtendableProps<PropsOf<C>, Properties>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
.iconWrapper {
display: flex;
}

View File

@@ -1,5 +1,65 @@
export {
MaterialIcon,
type MaterialIconProps,
type MaterialIconSetIconProps,
} from "./MaterialIcon"
import { HTMLAttributes } from "react"
import { MaterialIconName, materialIcons } from "./generated"
import { VariantProps } from "class-variance-authority"
import { iconVariants } from "../variants"
import styles from "./index.module.css"
export interface MaterialIconProps
extends
Omit<HTMLAttributes<HTMLOrSVGElement>, "color" | "id">,
VariantProps<typeof iconVariants> {
icon: MaterialIconName
size?: number
styleName?: "outlined" | "rounded" | "sharp"
isFilled?: boolean
}
export type MaterialIconSetIconProps = Omit<MaterialIconProps, "icon">
export function MaterialIcon({
icon,
size = 24,
styleName = "outlined",
color = "CurrentColor",
isFilled,
...props
}: MaterialIconProps) {
const iconStyles = materialIcons[icon]
if (!iconStyles) {
console.warn(`Icon "${icon}" not found in registry`)
return null
}
const styleVariants = iconStyles[styleName]
if (!styleVariants) {
console.warn(`Icon "${icon}" does not have style "${styleName}"`)
return null
}
const IconComponent = isFilled ? styleVariants.filled : styleVariants.outlined
if (!IconComponent) {
console.warn(
`Icon "${icon}" does not have ${
isFilled ? "filled" : "outlined"
} variant for style "${styleName}"`
)
return null
}
const iconClassName = iconVariants({ color })
return (
<span className={`${styles.iconWrapper}`} {...props}>
<IconComponent
width={size}
height={size}
className={iconClassName}
aria-hidden
focusable={false}
/>
</span>
)
}