Feat/BOOK-117 svg accessibility

* feat(BOOK-117): Added aria-label to Scandic Friends levels
* feat(BOOK-117): Added aria-label to hotel logos
* feat(BOOK-117): Added alt text to app download images
* feat(BOOK-117): Added same logo component to footer as the one in the header
* feat(BOOK-117): Added aria attributes to icons similar to how we handled MaterialIcon aria attributes

Approved-by: Bianca Widstam
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-11-13 06:34:18 +00:00
parent c4b564998c
commit ce469bc4b4
117 changed files with 541 additions and 247 deletions

View File

@@ -6,6 +6,8 @@ export function DowntownCamperLogoLarge({ className }: { className?: string }) {
viewBox="0 0 308 315"
fill="none"
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Downtown Camper by Scandic"
className={className}
>
<g clipPath="url(#clip0_11502_35791)">

View File

@@ -2,6 +2,8 @@ export function DowntownCamperLogoSmall({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Downtown Camper"
width="180"
height="36"
viewBox="0 0 180 36"

View File

@@ -6,6 +6,8 @@ export function GrandHotelLogoLarge({ className }: { className?: string }) {
viewBox="0 0 308 315"
fill="none"
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Grand Hotel Oslo by Scandic"
className={className}
>
<path

View File

@@ -2,6 +2,8 @@ export function GrandHotelLogoSmall({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Grand Hotel Oslo"
width="88"
height="34"
viewBox="0 0 88 34"

View File

@@ -6,6 +6,8 @@ export function HaymarketLogoLarge({ className }: { className?: string }) {
viewBox="0 0 308 315"
fill="none"
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Haymarket by Scandic"
className={className}
>
<path

View File

@@ -2,6 +2,8 @@ export function HaymarketLogoSmall({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Haymarket"
width="139"
height="36"
viewBox="0 0 139 36"

View File

@@ -6,6 +6,8 @@ export function HotelNorgeLogoLarge({ className }: { className?: string }) {
viewBox="0 0 308 315"
fill="none"
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Hotel Norge by Scandic"
className={className}
>
<path

View File

@@ -2,6 +2,8 @@ export function HotelNorgeLogoSmall({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Hotel Norge"
width="175"
height="36"
viewBox="0 0 175 36"

View File

@@ -6,6 +6,8 @@ export function MarskiLogoLarge({ className }: { className?: string }) {
viewBox="0 0 308 315"
fill="none"
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Marski by Scandic"
className={className}
>
<path

View File

@@ -2,6 +2,8 @@ export function MarskiLogoSmall({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Marski"
width="236"
height="36"
viewBox="0 0 236 36"

View File

@@ -6,6 +6,8 @@ export function ScandicGoLogoLarge({ className }: { className?: string }) {
viewBox="0 0 308 315"
fill="none"
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Scandic Go"
className={className}
>
<path

View File

@@ -2,6 +2,8 @@ export function ScandicGoLogoSmall({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Scandic Go"
width="206"
height="36"
viewBox="0 0 206 36"

View File

@@ -6,6 +6,8 @@ export function TheDockLogoLarge({ className }: { className?: string }) {
viewBox="0 0 308 315"
fill="none"
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="The Dock by Scandic"
className={className}
>
<g clipPath="url(#clip0_12068_37172)">

View File

@@ -6,6 +6,8 @@ export function TheDockLogoSmall({ className }: { className?: string }) {
viewBox="0 0 288 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="The Dock"
className={className}
>
<path

View File

@@ -1,4 +1,3 @@
import Image from "@scandic-hotels/design-system/Image"
import Link from "@scandic-hotels/design-system/OldDSLink"
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -7,30 +6,21 @@ import { getFooter } from "@/lib/trpc/memoizedRequests"
import LanguageSwitcher from "@/components/LanguageSwitcher"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import { LogoLink } from "../../LogoLink"
import SocialLink from "./SocialLink"
import styles from "./details.module.css"
export default async function FooterDetails() {
const lang = await getLang()
const intl = await getIntl()
// preloaded
const footer = await getFooter()
const currentYear = new Date().getFullYear()
return (
<div className={styles.details}>
<div className={styles.topContainer}>
<Link href={`/${lang}`}>
<Image
alt="Scandic Hotels logo"
height={22}
src="/_static/img/scandic-logotype-white.svg"
width={103}
/>
</Link>
<LogoLink isInverted />
<nav className={styles.socialNav}>
{footer?.socialMedia.links.map(
({ href }) => href && <SocialLink link={href} key={href.title} />
@@ -73,21 +63,13 @@ export default async function FooterDetails() {
}
export async function FooterDetailsSkeleton() {
const lang = await getLang()
const intl = await getIntl()
const currentYear = new Date().getFullYear()
return (
<section className={styles.details}>
<div className={styles.topContainer}>
<Link href={`/${lang}`}>
<Image
alt="Scandic Hotels logo"
height={22}
src="/_static/img/scandic-logotype-white.svg"
width={103}
/>
</Link>
<LogoLink isInverted />
<nav className={styles.socialNav}>
<SkeletonShimmer width="10ch" height="20px" contrast="dark" />
</nav>

View File

@@ -1,5 +1,7 @@
"use client"
import { useIntl } from "react-intl"
import Image from "@scandic-hotels/design-system/Image"
import Link from "@scandic-hotels/design-system/OldDSLink"
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
@@ -8,9 +10,10 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import useLang from "@/hooks/useLang"
import { trackFooterClick, trackSocialMediaClick } from "@/utils/tracking"
import { getAppDownloadAttributes } from "./utils"
import styles from "./secondarynav.module.css"
import { AppDownLoadLinks } from "@/types/components/footer/appDownloadIcons"
import { type FooterSecondaryNavProps } from "@/types/components/footer/navigation"
export default function FooterSecondaryNav({
@@ -18,6 +21,7 @@ export default function FooterSecondaryNav({
appDownloads,
}: FooterSecondaryNavProps) {
const lang = useLang()
const intl = useIntl()
return (
<div className={styles.secondaryNavigation}>
@@ -27,30 +31,28 @@ export default function FooterSecondaryNav({
</Typography>
{appDownloads.links.length ? (
<ul className={styles.secondaryNavigationList}>
{appDownloads.links.map(
({ href, type }) =>
href && (
<li key={type}>
<a
href={href.href}
target="_blank"
aria-label={href.title}
onClick={() => trackSocialMediaClick(href.title)}
>
<Image
src={
AppDownLoadLinks[
`${type}_${lang}` as keyof typeof AppDownLoadLinks
]
}
alt={href.title}
width={125}
height={40}
/>
</a>
</li>
)
)}
{appDownloads.links.map(({ href, type }) => {
const attributes = getAppDownloadAttributes(
intl,
`${type}_${lang}`
)
return href && attributes ? (
<li key={type}>
<a
href={href.href}
target="_blank"
onClick={() => trackSocialMediaClick(href.title)}
>
<Image
src={attributes.src}
alt={attributes.alt}
width={125}
height={40}
/>
</a>
</li>
) : null
})}
</ul>
) : null}
</nav>

View File

@@ -0,0 +1,77 @@
import type { IntlShape } from "react-intl"
export function getAppDownloadAttributes(intl: IntlShape, key: string) {
const appleAlt = intl.formatMessage({
id: "footer.appDownloadAlt.apple",
defaultMessage: "Download on the App Store",
})
const googleAlt = intl.formatMessage({
id: "footer.appDownloadAlt.google",
defaultMessage: "Get it on Google Play",
})
switch (key) {
case "Apple_da":
return {
src: "/_static/img/store-badges/app-store-badge-da.svg",
alt: appleAlt,
}
case "Apple_de":
return {
src: "/_static/img/store-badges/app-store-badge-de.svg",
alt: appleAlt,
}
case "Apple_en":
return {
src: "/_static/img/store-badges/app-store-badge-en.svg",
alt: appleAlt,
}
case "Apple_fi":
return {
src: "/_static/img/store-badges/app-store-badge-fi.svg",
alt: appleAlt,
}
case "Apple_no":
return {
src: "/_static/img/store-badges/app-store-badge-no.svg",
alt: appleAlt,
}
case "Apple_sv":
return {
src: "/_static/img/store-badges/app-store-badge-sv.svg",
alt: appleAlt,
}
case "Google_da":
return {
src: "/_static/img/store-badges/google-play-badge-da.svg",
alt: googleAlt,
}
case "Google_de":
return {
src: "/_static/img/store-badges/google-play-badge-de.svg",
alt: googleAlt,
}
case "Google_en":
return {
src: "/_static/img/store-badges/google-play-badge-en.svg",
alt: googleAlt,
}
case "Google_fi":
return {
src: "/_static/img/store-badges/google-play-badge-fi.svg",
alt: googleAlt,
}
case "Google_no":
return {
src: "/_static/img/store-badges/google-play-badge-no.svg",
alt: googleAlt,
}
case "Google_sv":
return {
src: "/_static/img/store-badges/google-play-badge-sv.svg",
alt: googleAlt,
}
default:
return null
}
}

View File

@@ -1,8 +0,0 @@
.logoLink {
display: inline-flex;
width: auto;
}
:global(body:has(.themed-hotel-page)) .logoIcon {
color: var(--Surface-UI-Fill-Intense);
}

View File

@@ -1,7 +1,7 @@
import { Suspense } from "react"
import { LogoLink } from "../../LogoLink"
import { NavigationMenuListSkeleton } from "./NavigationMenu/NavigationMenuList"
import { LogoLink } from "./LogoLink"
import { MobileMenuSkeleton } from "./MobileMenu"
import MobileMenuWrapper from "./MobileMenuWrapper"
import MyPagesMenuWrapper from "./MyPagesMenuWrapper"

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function BestFriend({
className,
"aria-label": ariaLabel,
color,
height = "75",
width = "159",
@@ -21,6 +22,8 @@ export default function BestFriend({
viewBox="0 0 159 75"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "BEST FRIEND"}
{...props}
>
<g>

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function CloseFriend({
className,
"aria-label": ariaLabel,
color,
height = "75",
width = "158",
@@ -21,6 +22,8 @@ export default function CloseFriend({
viewBox="0 0 158 75"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "CLOSE FRIEND"}
{...props}
>
<g>

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function DearFriend({
className,
"aria-label": ariaLabel,
color,
height = "75",
width = "159",
@@ -21,6 +22,8 @@ export default function DearFriend({
viewBox="0 0 159 75"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "DEAR FRIEND"}
{...props}
>
<g>

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function GoodFriend({
className,
"aria-label": ariaLabel,
color,
height = "75",
width = "159",
@@ -21,6 +22,8 @@ export default function GoodFriend({
viewBox="0 0 159 75"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "GOOD FRIEND"}
{...props}
>
<g>

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function LoyalFriend({
className,
"aria-label": ariaLabel,
color,
height = "75",
width = "158",
@@ -21,6 +22,8 @@ export default function LoyalFriend({
viewBox="0 0 158 75"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "LOYAL FRIEND"}
{...props}
>
<g>

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function NewFriend({
className,
"aria-label": ariaLabel,
color,
height = "75",
width = "159",
@@ -21,6 +22,8 @@ export default function NewFriend({
viewBox="0 0 159 75"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "NEW FRIEND"}
{...props}
>
<g>

View File

@@ -2,7 +2,11 @@ import { levelVariants } from "../../variants"
import type { LevelProps } from "../../levels"
export default function ScandicFriends({ className, color }: LevelProps) {
export default function ScandicFriends({
className,
"aria-label": ariaLabel,
color,
}: LevelProps) {
const classNames = levelVariants({
className,
color,
@@ -11,6 +15,8 @@ export default function ScandicFriends({ className, color }: LevelProps) {
<svg
className={classNames}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "SCANDIC FRIENDS"}
width="197"
height="75"
viewBox="0 0 197 75"

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function TrueFriend({
className,
"aria-label": ariaLabel,
color,
height = "75",
width = "159",
@@ -21,6 +22,8 @@ export default function TrueFriend({
viewBox="0 0 159 75"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "TRUE FRIEND"}
{...props}
>
<g>

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function BestFriend({
className,
"aria-label": ariaLabel,
color,
height = "44",
width = "274",
@@ -21,6 +22,8 @@ export default function BestFriend({
viewBox="0 0 274 44"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "BEST FRIEND"}
{...props}
>
<path

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function CloseFriend({
className,
"aria-label": ariaLabel,
color,
height = "44",
width = "310",
@@ -21,6 +22,8 @@ export default function CloseFriend({
viewBox="0 0 310 44"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "CLOSE FRIEND"}
{...props}
>
<path

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function DearFriend({
className,
"aria-label": ariaLabel,
color,
height = "44",
width = "294",
@@ -21,6 +22,8 @@ export default function DearFriend({
viewBox="0 0 294 44"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "DEAR FRIEND"}
{...props}
>
<path

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function GoodFriend({
className,
"aria-label": ariaLabel,
color,
height = "44",
width = "310",
@@ -21,6 +22,8 @@ export default function GoodFriend({
viewBox="0 0 310 44"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "GOOD FRIEND"}
{...props}
>
<path

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function LoyalFriend({
className,
"aria-label": ariaLabel,
color,
height = "44",
width = "305",
@@ -21,6 +22,8 @@ export default function LoyalFriend({
viewBox="0 0 305 44"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "LOYAL FRIEND"}
{...props}
>
<path

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function NewFriend({
className,
"aria-label": ariaLabel,
color,
height = "44",
width = "275",
@@ -21,6 +22,8 @@ export default function NewFriend({
viewBox="0 0 275 44"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "NEW FRIEND"}
{...props}
>
<path

View File

@@ -4,6 +4,7 @@ import type { LevelProps } from "../../levels"
export default function TrueFriend({
className,
"aria-label": ariaLabel,
color,
height = "44",
width = "284",
@@ -21,6 +22,8 @@ export default function TrueFriend({
viewBox="0 0 284 44"
width={width}
xmlns="http://www.w3.org/2000/svg"
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label={ariaLabel ?? "TRUE FRIEND"}
{...props}
>
<path

View File

@@ -1,4 +1,5 @@
"use client"
import { cx } from "class-variance-authority"
import NextLink from "next/link"
import { useIntl } from "react-intl"
@@ -12,7 +13,15 @@ import styles from "./logoLink.module.css"
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
export function LogoLink() {
interface LogoLinkProps extends React.HTMLAttributes<HTMLAnchorElement> {
isInverted?: boolean
}
export function LogoLink({
isInverted = false,
"aria-label": ariaLabel,
...props
}: LogoLinkProps) {
const lang = useLang()
const intl = useIntl()
const { toggleDropdown, isHamburgerMenuOpen } = useDropdownStore()
@@ -28,13 +37,19 @@ export function LogoLink() {
className={styles.logoLink}
href={`/${lang}`}
onClick={handleNavigate}
aria-label={intl.formatMessage({
id: "header.backToScandicHotelsCom",
defaultMessage: "Back to scandichotels.com",
})}
aria-label={
ariaLabel ??
intl.formatMessage({
id: "header.backToScandicHotelsCom",
defaultMessage: "Back to scandichotels.com",
})
}
{...props}
>
<ScandicLogoIcon
className={styles.logoIcon}
className={cx(styles.logoIcon, {
[styles.inverted]: isInverted,
})}
width="103px"
height="22px"
/>

View File

@@ -0,0 +1,12 @@
.logoLink {
display: inline-flex;
width: auto;
}
.logoIcon.inverted {
color: var(--Icon-Inverted);
}
:global(body:has(.themed-hotel-page)) .logoIcon:not(.inverted) {
color: var(--Surface-UI-Fill-Intense);
}

View File

@@ -1,14 +0,0 @@
export enum AppDownLoadLinks {
Apple_da = "/_static/img/store-badges/app-store-badge-da.svg",
Apple_de = "/_static/img/store-badges/app-store-badge-de.svg",
Apple_en = "/_static/img/store-badges/app-store-badge-en.svg",
Apple_fi = "/_static/img/store-badges/app-store-badge-fi.svg",
Apple_no = "/_static/img/store-badges/app-store-badge-no.svg",
Apple_sv = "/_static/img/store-badges/app-store-badge-sv.svg",
Google_da = "/_static/img/store-badges/google-play-badge-da.svg",
Google_de = "/_static/img/store-badges/google-play-badge-de.svg",
Google_en = "/_static/img/store-badges/google-play-badge-en.svg",
Google_fi = "/_static/img/store-badges/google-play-badge-fi.svg",
Google_no = "/_static/img/store-badges/google-play-badge-no.svg",
Google_sv = "/_static/img/store-badges/google-play-badge-sv.svg",
}