Merged in fix/BOOK-119-accessibility-redemption-radiogroup (pull request #3172)

fix(BOOK-119): reward nights accessible radiogroup

* fix(BOOK-119): reward nights accessible radiogroup

* fix(BOOK-119): pr comment focus

* fix(BOOK-119): added roomtype name to the radiogroup for accessibility improvment


Approved-by: Matilda Haneling
Approved-by: Erik Tiekstra
This commit is contained in:
Bianca Widstam
2025-11-20 13:28:21 +00:00
parent b1d7fbad88
commit ebd6e1dc2c
12 changed files with 51 additions and 13 deletions

View File

@@ -11,9 +11,10 @@ import type { RoomInfo } from "../../../../../../../contexts/SelectRate/types"
type Props = { type Props = {
roomInfo: RoomInfo roomInfo: RoomInfo
roomTypeCode: string
} }
export default function Details({ roomInfo }: Props) { export default function Details({ roomInfo, roomTypeCode }: Props) {
const intl = useIntl() const intl = useIntl()
const { name, occupancy, roomSize } = roomInfo || {} const { name, occupancy, roomSize } = roomInfo || {}
@@ -50,7 +51,7 @@ export default function Details({ roomInfo }: Props) {
</div> </div>
<div className={styles.roomDetails}> <div className={styles.roomDetails}>
<Typography variant="Title/Subtitle/lg"> <Typography variant="Title/Subtitle/lg">
<h2>{name}</h2> <h2 id={roomTypeCode}>{name}</h2>
</Typography> </Typography>
</div> </div>
</> </>

View File

@@ -227,6 +227,7 @@ function Inner({
key={product.rate} key={product.rate}
approximateRate={approximateRate} approximateRate={approximateRate}
bannerText={bannerText} bannerText={bannerText}
roomTypeCode={roomTypeCode}
handleChange={() => handleChange={() =>
selectRate({ selectRate({
roomIndex, roomIndex,

View File

@@ -221,6 +221,7 @@ function CorporateChequeCode({
handleChange={() => handleChange={() =>
handleSelectRate(codeProduct.corporateCheque.rateCode) handleSelectRate(codeProduct.corporateCheque.rateCode)
} }
roomTypeCode={roomTypeCode}
isSelected={isSelected} isSelected={isSelected}
id={`${roomNr}-${roomTypeCode}-${rateCode}`.replace(/\s+/g, "-")} id={`${roomNr}-${roomTypeCode}-${rateCode}`.replace(/\s+/g, "-")}
paymentTerm={rateTitles[codeProduct.rate].paymentTerm} paymentTerm={rateTitles[codeProduct.rate].paymentTerm}
@@ -314,6 +315,7 @@ function PublicCode({
approximateRate={approximateRate} approximateRate={approximateRate}
bannerText={bannerText} bannerText={bannerText}
comparisonRate={comparisonRate} comparisonRate={comparisonRate}
roomTypeCode={roomTypeCode}
handleChange={() => handleSelectRate(codeProduct.public!.rateCode)} handleChange={() => handleSelectRate(codeProduct.public!.rateCode)}
isSelected={isSelected} isSelected={isSelected}
id={`${roomNr}-${roomTypeCode}-${rateCode}`.replace(/\s+/g, "-")} id={`${roomNr}-${roomTypeCode}-${rateCode}`.replace(/\s+/g, "-")}
@@ -379,6 +381,7 @@ function VoucherCode({
bannerText={bannerText} bannerText={bannerText}
handleChange={() => handleSelectRate(codeProduct.voucher.rateCode)} handleChange={() => handleSelectRate(codeProduct.voucher.rateCode)}
isSelected={isSelected} isSelected={isSelected}
roomTypeCode={roomTypeCode}
id={`${roomNr}-${roomTypeCode}-${rateCode}`.replace(/\s+/g, "-")} id={`${roomNr}-${roomTypeCode}-${rateCode}`.replace(/\s+/g, "-")}
paymentTerm={rateTitles[codeProduct.rate].paymentTerm} paymentTerm={rateTitles[codeProduct.rate].paymentTerm}
rate={{ rate={{

View File

@@ -34,6 +34,7 @@ export default function Redemptions({
actions: { selectRate }, actions: { selectRate },
selectedRates, selectedRates,
} = useSelectRateContext() } = useSelectRateContext()
const roomNr = roomIndex + 1
const pointsCurrency = useGetPointsCurrency() const pointsCurrency = useGetPointsCurrency()
// TODO: Replace with context value when we have support for dropdown "Show all rates" // TODO: Replace with context value when we have support for dropdown "Show all rates"
@@ -125,10 +126,12 @@ export default function Redemptions({
}} }}
paymentTerm={rateTitles[firstRedemption.rate].paymentTerm} paymentTerm={rateTitles[firstRedemption.rate].paymentTerm}
rates={rates} rates={rates}
id={`${roomNr}-${roomTypeCode}`.replace(/\s+/g, "-")}
rateTitle={rateTitles[firstRedemption.rate].title} rateTitle={rateTitles[firstRedemption.rate].title}
rateTermDetails={rateTermDetails} rateTermDetails={rateTermDetails}
selectedRate={selectedRateCode} selectedRate={selectedRateCode}
isNotEnoughPoints={notEnoughPoints} isNotEnoughPoints={notEnoughPoints}
roomTypeCode={roomTypeCode}
notEnoughPointsText={intl.formatMessage({ notEnoughPointsText={intl.formatMessage({
id: "booking.notEnoughPoints", id: "booking.notEnoughPoints",
defaultMessage: "Not enough points", defaultMessage: "Not enough points",

View File

@@ -250,6 +250,7 @@ function Inner({
}} }}
isMemberRateActive={isMemberRateActive} isMemberRateActive={isMemberRateActive}
isSelected={isSelected} isSelected={isSelected}
roomTypeCode={roomTypeCode}
id={`${roomNr}-${roomTypeCode}-${rateCode}`.replace(/\s+/g, "-")} id={`${roomNr}-${roomTypeCode}-${rateCode}`.replace(/\s+/g, "-")}
paymentTerm={rateTitles[product.rate].paymentTerm} paymentTerm={rateTitles[product.rate].paymentTerm}
rateTitle={rateTitles[product.rate].title} rateTitle={rateTitles[product.rate].title}

View File

@@ -47,7 +47,7 @@ export function RoomListItem({
images={room.roomInfo.images ?? []} images={room.roomInfo.images ?? []}
hotelId={hotelId} hotelId={hotelId}
/> />
<Details roomInfo={room.roomInfo} /> <Details roomInfo={room.roomInfo} roomTypeCode={room.roomTypeCode} />
</div> </div>
<div className={styles.container}> <div className={styles.container}>
{room.status === AvailabilityEnum.NotAvailable ? ( {room.status === AvailabilityEnum.NotAvailable ? (

View File

@@ -2,6 +2,7 @@ import { PropsWithChildren } from 'react'
import { Radio as AriaRadio } from 'react-aria-components' import { Radio as AriaRadio } from 'react-aria-components'
import styles from './radio.module.css' import styles from './radio.module.css'
import { variants } from './variants' import { variants } from './variants'
import { cx } from 'class-variance-authority'
interface RadioProps extends PropsWithChildren { interface RadioProps extends PropsWithChildren {
value: string value: string
@@ -22,7 +23,7 @@ export function Radio({ id, value, children, color, isDisabled }: RadioProps) {
id={inputId} id={inputId}
value={value} value={value}
isDisabled={isDisabled} isDisabled={isDisabled}
className={`${styles.container} ${isDisabled ? styles.disabled : ''}`} className={cx(styles.container, { [styles.disabled]: isDisabled })}
> >
<div className={`${styles.radio} ${classNames}`} /> <div className={`${styles.radio} ${classNames}`} />
<div>{children}</div> <div>{children}</div>

View File

@@ -22,6 +22,11 @@
border-width: 8px; border-width: 8px;
} }
.container[data-focus-visible] .radio {
outline: 2px solid var(--UI-Input-Controls-Border-Focus);
outline-offset: 2px;
}
.disabled { .disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;

View File

@@ -24,6 +24,7 @@ interface CampaignRateCardProps {
isHighlightedRate?: boolean isHighlightedRate?: boolean
isHighlightedRateLabel?: boolean isHighlightedRateLabel?: boolean
approximateRate?: Rate approximateRate?: Rate
roomTypeCode: string
handleChange: () => void handleChange: () => void
handleTermsClick?: () => void handleTermsClick?: () => void
rateTermDetails: RateTermDetails[] rateTermDetails: RateTermDetails[]
@@ -35,6 +36,7 @@ export default function CampaignRateCard({
rateTitle, rateTitle,
paymentTerm, paymentTerm,
rate, rate,
roomTypeCode,
memberRate, memberRate,
approximateRate, approximateRate,
comparisonRate, comparisonRate,
@@ -60,8 +62,8 @@ export default function CampaignRateCard({
onPress={handleChange} onPress={handleChange}
className={styles.buttonOverlay} className={styles.buttonOverlay}
aria-pressed={isSelected} aria-pressed={isSelected}
aria-labelledby={`${id}-title`} aria-describedby={`${roomTypeCode} ${id}-title`}
aria-describedby={`${id}-details`} aria-labelledby={`${id}-details`}
/> />
<div className={styles.banner}> <div className={styles.banner}>
<MaterialIcon size={16} icon="sell" color="CurrentColor" /> <MaterialIcon size={16} icon="sell" color="CurrentColor" />

View File

@@ -19,6 +19,7 @@ interface CodeRateCardProps {
bannerText: string bannerText: string
comparisonRate?: Omit<Rate, 'label'> comparisonRate?: Omit<Rate, 'label'>
approximateRate?: Rate approximateRate?: Rate
roomTypeCode: string
isHighlightedRate?: boolean isHighlightedRate?: boolean
handleChange: () => void handleChange: () => void
handleTermsClick?: () => void handleTermsClick?: () => void
@@ -31,6 +32,7 @@ export default function CodeRateCard({
rateTitle, rateTitle,
paymentTerm, paymentTerm,
rate, rate,
roomTypeCode,
approximateRate, approximateRate,
comparisonRate, comparisonRate,
bannerText, bannerText,
@@ -53,8 +55,8 @@ export default function CodeRateCard({
onPress={handleChange} onPress={handleChange}
className={styles.buttonOverlay} className={styles.buttonOverlay}
aria-pressed={isSelected} aria-pressed={isSelected}
aria-labelledby={`${id}-title`} aria-describedby={`${roomTypeCode} ${id}-title`}
aria-describedby={`${id}-details`} aria-labelledby={`${id}-details`}
/> />
<Typography variant="Label/xsBold"> <Typography variant="Label/xsBold">
<p className={styles.banner}>{bannerText}</p> <p className={styles.banner}>{bannerText}</p>

View File

@@ -8,6 +8,7 @@ import { Radio } from '../../Radio'
import Modal from '../Modal' import Modal from '../Modal'
import styles from '../rate-card.module.css' import styles from '../rate-card.module.css'
import { variants } from '../variants' import { variants } from '../variants'
import { useIntl } from 'react-intl'
interface PointsRateCardProps { interface PointsRateCardProps {
rateTitle: string rateTitle: string
@@ -19,22 +20,27 @@ interface PointsRateCardProps {
isNotEnoughPoints?: boolean isNotEnoughPoints?: boolean
notEnoughPointsText?: string notEnoughPointsText?: string
rateTermDetails: RateTermDetails[] rateTermDetails: RateTermDetails[]
id: string
roomTypeCode: string
} }
export default function PointsRateCard({ export default function PointsRateCard({
rateTitle, rateTitle,
paymentTerm, paymentTerm,
bannerText, bannerText,
id,
rates, rates,
selectedRate, selectedRate,
isNotEnoughPoints, isNotEnoughPoints,
notEnoughPointsText, notEnoughPointsText,
onRateSelect, onRateSelect,
roomTypeCode,
rateTermDetails, rateTermDetails,
}: PointsRateCardProps) { }: PointsRateCardProps) {
const classNames = variants({ const classNames = variants({
variant: 'Points', variant: 'Points',
}) })
const intl = useIntl()
return ( return (
<div className={classNames}> <div className={classNames}>
@@ -49,7 +55,15 @@ export default function PointsRateCard({
title={rateTitle} title={rateTitle}
subtitle={paymentTerm} subtitle={paymentTerm}
trigger={ trigger={
<IconButton theme="Black" style="Muted" wrapping> <IconButton
theme="Black"
style="Muted"
wrapping
aria-label={intl.formatMessage({
id: 'selectRate.rateCard.openReservationPolicy',
defaultMessage: 'Open reservation policy',
})}
>
<MaterialIcon icon="info" size={20} color="Icon/Default" /> <MaterialIcon icon="info" size={20} color="Icon/Default" />
</IconButton> </IconButton>
} }
@@ -85,8 +99,10 @@ export default function PointsRateCard({
</header> </header>
<div className={styles.content}> <div className={styles.content}>
<RadioGroup <RadioGroup
aria-label={rateTitle} /* eslint-disable-next-line formatjs/no-literal-string-in-jsx */
value={selectedRate} aria-label={`${rateTitle} / ${paymentTerm}`}
aria-describedby={roomTypeCode}
value={selectedRate ? selectedRate : null} // default to null for focus to land on the first radio on tab, undefined set tabIndex to -1
onChange={onRateSelect} onChange={onRateSelect}
> >
{rates.map((rate, index) => ( {rates.map((rate, index) => (
@@ -94,6 +110,7 @@ export default function PointsRateCard({
<Radio <Radio
value={rate.rateCode} value={rate.rateCode}
isDisabled={rate.isDisabled || isNotEnoughPoints} isDisabled={rate.isDisabled || isNotEnoughPoints}
id={`${id}-${rate.rateCode}`}
> >
<div className={styles.pointsRow}> <div className={styles.pointsRow}>
<Typography variant="Title/Subtitle/md"> <Typography variant="Title/Subtitle/md">

View File

@@ -21,6 +21,7 @@ interface RegularRateCardProps {
approximateRate?: Rate approximateRate?: Rate
isMemberRateActive?: boolean isMemberRateActive?: boolean
handleChange: () => void handleChange: () => void
roomTypeCode: string
rateTermDetails: RateTermDetails[] rateTermDetails: RateTermDetails[]
} }
@@ -32,6 +33,7 @@ export default function RegularRateCard({
approximateRate, approximateRate,
omnibusRate, omnibusRate,
rate, rate,
roomTypeCode,
memberRate, memberRate,
isMemberRateActive, isMemberRateActive,
handleChange, handleChange,
@@ -51,8 +53,8 @@ export default function RegularRateCard({
onPress={handleChange} onPress={handleChange}
className={styles.buttonOverlay} className={styles.buttonOverlay}
aria-pressed={isSelected} aria-pressed={isSelected}
aria-labelledby={`${id}-title`} aria-describedby={`${roomTypeCode} ${id}-title`}
aria-describedby={`${id}-details`} aria-labelledby={`${id}-details`}
/> />
<div className={styles.container}> <div className={styles.container}>
<Typography variant="Tag/sm"> <Typography variant="Tag/sm">