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 {
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;
}
.item:focus-visible {
outline: 2px auto -webkit-focus-ring-color;
outline-offset: 1px;
}
.buttonWrapper {
position: absolute;
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.",
})}
/>
{savedCreditCards?.length ? (
<SelectPaymentMethod
paymentMethods={savedCreditCards.map((x) => ({
...x,
cardType: x.cardType as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
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>
<SelectPaymentMethod
paymentMethods={(savedCreditCards ?? []).map((card) => ({
...card,
cardType: card.cardType as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
formName={"paymentMethod"}
/>
</>
)}
</>

View File

@@ -20,11 +20,18 @@ export default function WrappedAncillaryCard({
return (
<div
tabIndex={0}
role="button"
onClick={() => {
selectAncillary(ancillary)
trackViewAncillary(ancillary, booking)
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
selectAncillary(ancillary)
trackViewAncillary(ancillary, booking)
}
}}
>
<AncillaryCard ancillary={ancillaryWithoutDescription} />
</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 { Divider } from "@scandic-hotels/design-system/Divider"
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 { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import Link from "@scandic-hotels/design-system/OldDSLink"
@@ -155,42 +153,17 @@ export default function Form() {
id="guarantee"
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
>
{savedCreditCards?.length ? (
<SelectPaymentMethod
formName="paymentMethod"
paymentMethods={savedCreditCards.map((x) => ({
...x,
cardType: x.type as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
/>
) : null}
<PaymentOptionsGroup
name="paymentMethod"
label={
savedCreditCards?.length
? intl
.formatMessage({
id: "common.other",
defaultMessage: "Other",
})
.toUpperCase()
: undefined
}
<SelectPaymentMethod
paymentMethods={(savedCreditCards ?? []).map((card) => ({
...card,
cardType: card.cardType as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
>
<PaymentOption
value={PaymentMethodEnum.card}
label={intl.formatMessage({
id: "common.creditCard",
defaultMessage: "Credit card",
})}
/>
</PaymentOptionsGroup>
formName="paymentMethod"
/>
<div className={styles.termsAndConditions}>
<Checkbox className={styles.checkbox} name="termsAndConditions">
<Typography variant="Body/Supporting text (caption)/smRegular">

View File

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

View File

@@ -5,3 +5,7 @@
gap: var(--Space-x1);
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;
outline: none;
}
.infoButton:focus-visible {
outline: 2px auto -webkit-focus-ring-color;
outline-offset: 1px;
}
.overlay {
align-items: center;

View File

@@ -9,12 +9,9 @@ import {
import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import useSetOverflowVisibleOnRA from "@scandic-hotels/common/hooks/useSetOverflowVisibleOnRA"
import { Button } from "@scandic-hotels/design-system/Button"
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 { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
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 type { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
interface GuaranteeProps {
@@ -112,9 +110,10 @@ export default function Guarantee({ savedCreditCards }: GuaranteeProps) {
</DialogTrigger>
</div>
</Checkbox>
{savedCreditCards?.length && guarantee ? (
{guarantee && (
<SelectPaymentMethod
paymentMethods={savedCreditCards.map((card) => ({
paymentMethods={(savedCreditCards ?? []).map((card) => ({
...card,
cardType: card.cardType as PaymentMethodEnum,
}))}
@@ -123,30 +122,7 @@ export default function Guarantee({ savedCreditCards }: GuaranteeProps) {
}}
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>
)
}

View File

@@ -17,6 +17,10 @@
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 {
padding: var(--Space-x2) var(--Space-x3);

View File

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

View File

@@ -1,7 +1,9 @@
import { useIntl } from 'react-intl'
import { Label } from 'react-aria-components'
import { PaymentOptionsGroup } from '../PaymentOption/PaymentOptionsGroup'
import { PaymentOption } from '../PaymentOption/PaymentOption'
import { Typography } from '../../../components/Typography'
import styles from './selectPaymentMethod.module.css'
@@ -29,34 +31,71 @@ export function SelectPaymentMethod({
formName,
}: SelectPaymentMethodProps) {
const intl = useIntl()
const mySavedCardsLabel = intl.formatMessage({
id: 'payment.mySavedCards',
defaultMessage: 'My saved cards',
})
const mySavedCardsLabel = paymentMethods.length
? intl.formatMessage({
id: 'payment.mySavedCards',
defaultMessage: 'My saved cards',
})
: undefined
const otherCardLabel = paymentMethods.length
? intl.formatMessage({
id: 'common.other',
defaultMessage: 'Other',
})
: undefined
const hasSavedCards = paymentMethods.length > 0
return (
<section className={styles.section}>
<PaymentOptionsGroup
name={formName}
label={mySavedCardsLabel}
className={styles.paymentOptionContainer}
onChange={onChange}
initalValue={
//set credit card as default checked if user has no saved cards
!hasSavedCards ? PAYMENT_METHOD_TITLES.card : undefined
}
>
{paymentMethods?.map((paymentMethods) => {
const label =
PAYMENT_METHOD_TITLES[paymentMethods.cardType] ||
paymentMethods.alias ||
paymentMethods.cardType
<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) => {
const label =
PAYMENT_METHOD_TITLES[paymentMethods.cardType] ||
paymentMethods.alias ||
paymentMethods.cardType
return (
<PaymentOption
key={paymentMethods.id}
value={paymentMethods.id as PaymentMethodEnum}
label={label}
cardNumber={paymentMethods.truncatedNumber}
/>
)
})}
return (
<PaymentOption
key={paymentMethods.id}
value={paymentMethods.id as PaymentMethodEnum}
label={label}
cardNumber={paymentMethods.truncatedNumber}
/>
)
})}
<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>
</section>
)

View File

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