Merged in fix/book-115-hidden-focus-indicators (pull request #2925)

Fix/book 115 hidden focus indicators

* added focus ring to "how it works" text and close button in modal

* fix(BOOK-115): added focus ring to Hotel Sidepeek close button

* fix(BOOK-115): enabled selecting ancillaries with keyboard nav

* fix(BOOK-115): added focus indicator to "View and print receipt" in Manage Stay

* fix(BOOK-105 & BOOK-115): combined the two radio groups in payment selection to one, fixes focus indicator issue

* fix(BOOK-115): added focus indicator to shortcut links

* fix(BOOK-115): updated ancillaries keyboard selection

* fix(BOOK-115): removed tabIndex from Link component

* fix(BOOK-115): fixed single payment radio button not focusable

* fix(BOOK-115): updated  to onKeyDown

* added id to "credit card"

* removed toUpperCase() on lables

* removed brackets

* moved the focus indicator to the DS Button component

* removed !important from ButtonLink css

* changed <label> to <fieldset> and <legend> and added aria-label to PaymentOptionGroup

* removed css class from sidepeek that was previously removed

* reverted changes and synced Guarantee radiogroup with Payment radiogroup to use same semantics

* removed duplicate label

* removed old sub heading


Approved-by: Erik Tiekstra
This commit is contained in:
Matilda Haneling
2025-11-07 07:58:14 +00:00
parent fdf124bd0c
commit 2d237b8d14
14 changed files with 124 additions and 121 deletions

View File

@@ -20,3 +20,8 @@
.listItem:last-child .link { .listItem:last-child .link {
border-radius: 0 0 var(--Corner-radius-md) var(--Corner-radius-md); border-radius: 0 0 var(--Corner-radius-md) var(--Corner-radius-md);
} }
.link:focus-visible {
outline: 2px auto -webkit-focus-ring-color;
outline-offset: -4px;
}

View File

@@ -17,6 +17,10 @@
min-width: 0; min-width: 0;
} }
.item:focus-visible {
outline: 2px auto -webkit-focus-ring-color;
outline-offset: 1px;
}
.buttonWrapper { .buttonWrapper {
position: absolute; position: absolute;
top: 50%; top: 50%;

View File

@@ -146,39 +146,16 @@ export default function ConfirmationStep({
"By adding a card you also guarantee your room booking for late arrival.", "By adding a card you also guarantee your room booking for late arrival.",
})} })}
/> />
{savedCreditCards?.length ? (
<SelectPaymentMethod <SelectPaymentMethod
paymentMethods={savedCreditCards.map((x) => ({ paymentMethods={(savedCreditCards ?? []).map((card) => ({
...x, ...card,
cardType: x.cardType as PaymentMethodEnum, cardType: card.cardType as PaymentMethodEnum,
}))} }))}
onChange={(method) => { onChange={(method) => {
trackUpdatePaymentMethod({ method }) trackUpdatePaymentMethod({ method })
}} }}
formName={"paymentMethod"} formName={"paymentMethod"}
/> />
) : null}
<PaymentOptionsGroup
name="paymentMethod"
label={
savedCreditCards?.length
? intl
.formatMessage({
id: "common.other",
defaultMessage: "Other",
})
.toUpperCase()
: undefined
}
>
<PaymentOption
value={PaymentMethodEnum.card}
label={intl.formatMessage({
id: "common.creditCard",
defaultMessage: "Credit card",
})}
/>
</PaymentOptionsGroup>
</> </>
)} )}
</> </>

View File

@@ -20,11 +20,18 @@ export default function WrappedAncillaryCard({
return ( return (
<div <div
tabIndex={0}
role="button" role="button"
onClick={() => { onClick={() => {
selectAncillary(ancillary) selectAncillary(ancillary)
trackViewAncillary(ancillary, booking) trackViewAncillary(ancillary, booking)
}} }}
onKeyDown={(e) => {
if (e.key === "Enter") {
selectAncillary(ancillary)
trackViewAncillary(ancillary, booking)
}
}}
> >
<AncillaryCard ancillary={ancillaryWithoutDescription} /> <AncillaryCard ancillary={ancillaryWithoutDescription} />
</div> </div>

View File

@@ -12,8 +12,6 @@ import { privacyPolicyRoutes } from "@scandic-hotels/common/constants/routes/pri
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod" import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner" import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import Link from "@scandic-hotels/design-system/OldDSLink" import Link from "@scandic-hotels/design-system/OldDSLink"
@@ -155,42 +153,17 @@ export default function Form() {
id="guarantee" id="guarantee"
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)} onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
> >
{savedCreditCards?.length ? (
<SelectPaymentMethod <SelectPaymentMethod
formName="paymentMethod" paymentMethods={(savedCreditCards ?? []).map((card) => ({
paymentMethods={savedCreditCards.map((x) => ({ ...card,
...x, cardType: card.cardType as PaymentMethodEnum,
cardType: x.type as PaymentMethodEnum,
}))} }))}
onChange={(method) => { onChange={(method) => {
trackUpdatePaymentMethod({ method }) trackUpdatePaymentMethod({ method })
}} }}
formName="paymentMethod"
/> />
) : null}
<PaymentOptionsGroup
name="paymentMethod"
label={
savedCreditCards?.length
? intl
.formatMessage({
id: "common.other",
defaultMessage: "Other",
})
.toUpperCase()
: undefined
}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
>
<PaymentOption
value={PaymentMethodEnum.card}
label={intl.formatMessage({
id: "common.creditCard",
defaultMessage: "Credit card",
})}
/>
</PaymentOptionsGroup>
<div className={styles.termsAndConditions}> <div className={styles.termsAndConditions}>
<Checkbox className={styles.checkbox} name="termsAndConditions"> <Checkbox className={styles.checkbox} name="termsAndConditions">
<Typography variant="Body/Supporting text (caption)/smRegular"> <Typography variant="Body/Supporting text (caption)/smRegular">

View File

@@ -17,6 +17,7 @@ import styles from "./view.module.css"
export default function ViewAndPrintReceipt() { export default function ViewAndPrintReceipt() {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const canDownloadInvoice = useMyStayStore( const canDownloadInvoice = useMyStayStore(
(state) => (state) =>
!state.bookedRoom.isCancelled && !state.bookedRoom.isCancelled &&

View File

@@ -5,3 +5,7 @@
gap: var(--Space-x1); gap: var(--Space-x1);
padding: var(--Space-x05) 0; padding: var(--Space-x05) 0;
} }
.download:focus-visible {
outline: 2px solid -webkit-focus-ring-color;
}

View File

@@ -38,6 +38,10 @@
justify-self: flex-end; justify-self: flex-end;
outline: none; outline: none;
} }
.infoButton:focus-visible {
outline: 2px auto -webkit-focus-ring-color;
outline-offset: 1px;
}
.overlay { .overlay {
align-items: center; align-items: center;

View File

@@ -9,12 +9,9 @@ import {
import { useWatch } from "react-hook-form" import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import useSetOverflowVisibleOnRA from "@scandic-hotels/common/hooks/useSetOverflowVisibleOnRA" import useSetOverflowVisibleOnRA from "@scandic-hotels/common/hooks/useSetOverflowVisibleOnRA"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox" import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod" import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -22,6 +19,7 @@ import { trackUpdatePaymentMethod } from "@scandic-hotels/tracking/payment"
import styles from "./guarantee.module.css" import styles from "./guarantee.module.css"
import type { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import type { CreditCard } from "@scandic-hotels/trpc/types/user" import type { CreditCard } from "@scandic-hotels/trpc/types/user"
interface GuaranteeProps { interface GuaranteeProps {
@@ -112,9 +110,10 @@ export default function Guarantee({ savedCreditCards }: GuaranteeProps) {
</DialogTrigger> </DialogTrigger>
</div> </div>
</Checkbox> </Checkbox>
{savedCreditCards?.length && guarantee ? (
{guarantee && (
<SelectPaymentMethod <SelectPaymentMethod
paymentMethods={savedCreditCards.map((card) => ({ paymentMethods={(savedCreditCards ?? []).map((card) => ({
...card, ...card,
cardType: card.cardType as PaymentMethodEnum, cardType: card.cardType as PaymentMethodEnum,
}))} }))}
@@ -123,30 +122,7 @@ export default function Guarantee({ savedCreditCards }: GuaranteeProps) {
}} }}
formName={"paymentMethod"} formName={"paymentMethod"}
/> />
) : null} )}
{guarantee ? (
<PaymentOptionsGroup
name="paymentMethod"
label={
savedCreditCards?.length
? intl
.formatMessage({
id: "common.other",
defaultMessage: "Other",
})
.toUpperCase()
: undefined
}
>
<PaymentOption
value={PaymentMethodEnum.card}
label={intl.formatMessage({
id: "common.creditCard",
defaultMessage: "Credit card",
})}
/>
</PaymentOptionsGroup>
) : null}
</div> </div>
) )
} }

View File

@@ -17,6 +17,10 @@
cursor: progress; cursor: progress;
} }
} }
.button:focus-visible {
outline: 2px auto -webkit-focus-ring-color; /*color should probably be cutomized for each variant later on*/
outline-offset: 1px;
}
.size-large { .size-large {
padding: var(--Space-x2) var(--Space-x3); padding: var(--Space-x2) var(--Space-x3);

View File

@@ -8,3 +8,8 @@
justify-content: center; justify-content: center;
gap: var(--Space-x05); gap: var(--Space-x05);
} }
.buttonLink:focus-visible {
outline: 2px auto -webkit-focus-ring-color;
outline-offset: 1px;
}

View File

@@ -2,7 +2,6 @@
import { Label, RadioGroup } from 'react-aria-components' import { Label, RadioGroup } from 'react-aria-components'
import { useController, useFormContext } from 'react-hook-form' import { useController, useFormContext } from 'react-hook-form'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { Typography } from '../../../components/Typography' import { Typography } from '../../../components/Typography'
@@ -11,6 +10,7 @@ interface PaymentOptionsGroupProps {
label?: string label?: string
children: ReactNode children: ReactNode
className?: string className?: string
initalValue?: string
onChange?: (newValue: string) => void onChange?: (newValue: string) => void
} }
@@ -20,6 +20,7 @@ export function PaymentOptionsGroup({
children, children,
className, className,
onChange, onChange,
initalValue,
}: PaymentOptionsGroupProps) { }: PaymentOptionsGroupProps) {
const { control } = useFormContext() const { control } = useFormContext()
@@ -36,7 +37,11 @@ export function PaymentOptionsGroup({
} }
return ( return (
<RadioGroup value={value} onChange={handleChange} className={className}> <RadioGroup
value={initalValue || value}
onChange={handleChange}
className={className}
>
{label ? ( {label ? (
<Typography variant="Title/Overline/sm"> <Typography variant="Title/Overline/sm">
<Label>{label}</Label> <Label>{label}</Label>

View File

@@ -1,7 +1,9 @@
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import { Label } from 'react-aria-components'
import { PaymentOptionsGroup } from '../PaymentOption/PaymentOptionsGroup' import { PaymentOptionsGroup } from '../PaymentOption/PaymentOptionsGroup'
import { PaymentOption } from '../PaymentOption/PaymentOption' import { PaymentOption } from '../PaymentOption/PaymentOption'
import { Typography } from '../../../components/Typography'
import styles from './selectPaymentMethod.module.css' import styles from './selectPaymentMethod.module.css'
@@ -29,19 +31,43 @@ export function SelectPaymentMethod({
formName, formName,
}: SelectPaymentMethodProps) { }: SelectPaymentMethodProps) {
const intl = useIntl() const intl = useIntl()
const mySavedCardsLabel = intl.formatMessage({ const mySavedCardsLabel = paymentMethods.length
? intl.formatMessage({
id: 'payment.mySavedCards', id: 'payment.mySavedCards',
defaultMessage: 'My saved cards', defaultMessage: 'My saved cards',
}) })
: undefined
const otherCardLabel = paymentMethods.length
? intl.formatMessage({
id: 'common.other',
defaultMessage: 'Other',
})
: undefined
const hasSavedCards = paymentMethods.length > 0
return ( return (
<section className={styles.section}> <section className={styles.section}>
<PaymentOptionsGroup <PaymentOptionsGroup
name={formName} name={formName}
label={mySavedCardsLabel}
className={styles.paymentOptionContainer} className={styles.paymentOptionContainer}
onChange={onChange} onChange={onChange}
initalValue={
//set credit card as default checked if user has no saved cards
!hasSavedCards ? PAYMENT_METHOD_TITLES.card : undefined
}
> >
<Label className="sr-only">
{intl.formatMessage({
id: 'enterDetails.guarantee.cardOptions',
defaultMessage: 'Card options',
})}
</Label>
{hasSavedCards ? (
<>
<Typography variant="Title/Overline/sm">
<span>{mySavedCardsLabel}</span>
</Typography>
{paymentMethods?.map((paymentMethods) => { {paymentMethods?.map((paymentMethods) => {
const label = const label =
PAYMENT_METHOD_TITLES[paymentMethods.cardType] || PAYMENT_METHOD_TITLES[paymentMethods.cardType] ||
@@ -57,6 +83,19 @@ export function SelectPaymentMethod({
/> />
) )
})} })}
<Typography variant="Title/Overline/sm">
<span>{otherCardLabel}</span>
</Typography>
</>
) : null}
<PaymentOption
value={PAYMENT_METHOD_TITLES.card as PaymentMethodEnum}
label={intl.formatMessage({
id: 'common.creditCard',
defaultMessage: 'Credit card',
})}
/>
</PaymentOptionsGroup> </PaymentOptionsGroup>
</section> </section>
) )

View File

@@ -2,7 +2,7 @@
import { useContext, useRef } from 'react' import { useContext, useRef } from 'react'
import { Dialog, Modal, ModalOverlay } from 'react-aria-components' import { Dialog, Modal, ModalOverlay } from 'react-aria-components'
import { IconButton } from '../IconButton'
import { MaterialIcon } from '../Icons/MaterialIcon' import { MaterialIcon } from '../Icons/MaterialIcon'
import { Typography } from '../Typography' import { Typography } from '../Typography'
@@ -10,7 +10,6 @@ import { SidePeekContext } from './SidePeekContext'
import SidePeekSEO from './SidePeekSEO' import SidePeekSEO from './SidePeekSEO'
import { IconButton } from '../IconButton'
import styles from './sidePeek.module.css' import styles from './sidePeek.module.css'
interface SidePeekProps { interface SidePeekProps {
@@ -63,8 +62,8 @@ export default function SidePeek({
<IconButton <IconButton
theme="Black" theme="Black"
style="Muted" style="Muted"
onPress={onClose}
aria-label={closeLabel} aria-label={closeLabel}
onPress={onClose}
> >
<MaterialIcon <MaterialIcon
icon="close" icon="close"