@autobusal/routes-order 1.31.2 → 1.31.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/RoutesOrder.tsx +20 -10
- package/Step4/Passenger/Passenger.tsx +29 -7
- package/Step5/Addons/Addons.tsx +7 -0
- package/Step5/Billing/Billing.tsx +36 -22
- package/Step5/Coupons/Coupons.tsx +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.31.4
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The checkout form's fields have accessible names.** Passenger and billing inputs had no `id`/`for` pairing, no `aria-label` and no wrapping label - programmatically anonymous on the one form that decides whether a sale completes. Ids are `useId`-prefixed, which is what stops `adult1_*`, `adult2_*` and `child1_*` colliding; a hardcoded id would have traded one accessibility bug for another.
|
|
8
|
+
- Coupon code and the add-on phone fields get an `aria-label`, having only ever had a placeholder - and a placeholder disappears the moment someone types.
|
|
9
|
+
|
|
10
|
+
## 1.31.3
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **The results page's facts/schedule block now renders on the very first screen**, not only once a date has been picked. `/bus-lines/:from/:to` - the dateless URL the sitemap lists and the prerender crawl snapshots - was always on step 1, so `<Sections>` (price/duration/departures + the per-route schedule table + the pair's JSON-LD) never mounted for a single search engine or non-JS crawler. It now also renders on step 1; the bare `/bus-lines` search form (no pair in the URL) is unaffected since it has nothing to fetch.
|
|
15
|
+
|
|
3
16
|
## 1.31.2
|
|
4
17
|
|
|
5
18
|
### Changed
|
package/RoutesOrder.tsx
CHANGED
|
@@ -317,16 +317,26 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
|
|
|
317
317
|
/>
|
|
318
318
|
) }
|
|
319
319
|
|
|
320
|
-
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
321
|
-
Roadmap Tier 3.2.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
320
|
+
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-03, widened 2026-08-11
|
|
321
|
+
Roadmap Tier 3.2. NOT gated on step2/step3 (as it was originally)
|
|
322
|
+
- it now also renders on showStep1, which is what actually matters:
|
|
323
|
+
/bus-lines/:from/:to is the URL the sitemap lists and the
|
|
324
|
+
prerender crawl snapshots, and that URL is ALWAYS on step 1 - a
|
|
325
|
+
date has not been picked yet, so step2/step3 never mount and the
|
|
326
|
+
block never rendered for a single crawler. A logged human visitor
|
|
327
|
+
benefits the same way a search engine does: price/duration/
|
|
328
|
+
schedule for the pair, visible before they commit to a date.
|
|
329
|
+
|
|
330
|
+
Bare /bus-lines (no :from/:to) leaves both props undefined,
|
|
331
|
+
useGetFacts short-circuits on that, and Sections' own guard
|
|
332
|
+
returns null with nothing fetched - so the generic search form
|
|
333
|
+
gains no empty box underneath it.
|
|
334
|
+
|
|
335
|
+
Past step 2 the reader has picked a coach and is filling in
|
|
336
|
+
passenger details - facts about the pair are noise at that point,
|
|
337
|
+
which is why it still stops at step 3 and does not follow onto
|
|
338
|
+
checkout. */ }
|
|
339
|
+
{ (showStep1 || showStep2 || showStep3) && (
|
|
330
340
|
<Sections
|
|
331
341
|
from={ fromCity?.slug ?? params.from }
|
|
332
342
|
to={ toCity?.slug ?? params.to }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
1
2
|
import { TFunction } from 'i18next';
|
|
2
3
|
import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';
|
|
3
4
|
import { Calendar, Gender, ChooseSeat, Required } from '@autobusal/common';
|
|
@@ -47,6 +48,20 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
47
48
|
|
|
48
49
|
const has = (field: string): boolean => fields.includes(field);
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Edited: Claude - Date: 2026-08-19
|
|
53
|
+
* Every field here was rendered as a bare <input> beside its label text,
|
|
54
|
+
* with no id and no association - so a screen reader announced the whole
|
|
55
|
+
* passenger block as a run of unnamed "edit text". The field NAMES are
|
|
56
|
+
* already unique per passenger (adult1_fn, adult2_fn), but they belong to
|
|
57
|
+
* the form payload; ids are page-global, so they get a useId prefix -
|
|
58
|
+
* this component renders once per passenger, and two forms on one page
|
|
59
|
+
* would otherwise emit duplicate ids, which is its own defect.
|
|
60
|
+
*/
|
|
61
|
+
const uid = useId();
|
|
62
|
+
|
|
63
|
+
const id = (field: string): string => `${ uid }${ field }`;
|
|
64
|
+
|
|
50
65
|
return (
|
|
51
66
|
<Container className="box">
|
|
52
67
|
<Title>
|
|
@@ -55,10 +70,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
55
70
|
|
|
56
71
|
<div className="column">
|
|
57
72
|
<div className="row">
|
|
58
|
-
<Required>{ t('routes_order.step4.first_name') }</Required>
|
|
73
|
+
<Required htmlFor={ id(names.firstName) }>{ t('routes_order.step4.first_name') }</Required>
|
|
59
74
|
|
|
60
75
|
<input
|
|
61
76
|
type="text"
|
|
77
|
+
id={ id(names.firstName) }
|
|
62
78
|
defaultValue={ values !== undefined ? values[names.firstName] : '' }
|
|
63
79
|
{ ...refs(names.firstName, Validate('required|min_length:2|max_length:100', t)) }
|
|
64
80
|
/>
|
|
@@ -67,10 +83,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
67
83
|
</div>
|
|
68
84
|
|
|
69
85
|
<div className="row">
|
|
70
|
-
<Required>{ t('routes_order.step4.last_name') }</Required>
|
|
86
|
+
<Required htmlFor={ id(names.lastName) }>{ t('routes_order.step4.last_name') }</Required>
|
|
71
87
|
|
|
72
88
|
<input
|
|
73
89
|
type="text"
|
|
90
|
+
id={ id(names.lastName) }
|
|
74
91
|
defaultValue={ values !== undefined ? values[names.lastName] : '' }
|
|
75
92
|
{ ...refs(names.lastName, Validate('required|min_length:2|max_length:100', t)) }
|
|
76
93
|
/>
|
|
@@ -81,11 +98,12 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
81
98
|
|
|
82
99
|
<div className="column">
|
|
83
100
|
<div className="row">
|
|
84
|
-
<Required>{ t('routes_order.step4.dob') }</Required>
|
|
101
|
+
<Required htmlFor={ id(names.dob) }>{ t('routes_order.step4.dob') }</Required>
|
|
85
102
|
|
|
86
103
|
<Calendar
|
|
87
104
|
type="dob"
|
|
88
105
|
name={ names.dob }
|
|
106
|
+
id={ id(names.dob) }
|
|
89
107
|
defaultValue={ values !== undefined ? String(values[names.dob]) : undefined }
|
|
90
108
|
t={ t }
|
|
91
109
|
validation="required"
|
|
@@ -116,10 +134,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
116
134
|
<div className="column">
|
|
117
135
|
{ has('passport') && (
|
|
118
136
|
<div className="row">
|
|
119
|
-
<Required>{ t('routes_order.step4.passport') }</Required>
|
|
137
|
+
<Required htmlFor={ id(names.passport) }>{ t('routes_order.step4.passport') }</Required>
|
|
120
138
|
|
|
121
139
|
<input
|
|
122
140
|
type="text"
|
|
141
|
+
id={ id(names.passport) }
|
|
123
142
|
defaultValue={ values !== undefined ? values[names.passport] : '' }
|
|
124
143
|
{ ...refs(names.passport, Validate('required|min_length:2|max_length:50', t)) }
|
|
125
144
|
/>
|
|
@@ -130,10 +149,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
130
149
|
|
|
131
150
|
{ has('phone') && (
|
|
132
151
|
<div className="row">
|
|
133
|
-
<Required>{ t('routes_order.step4.phone') }</Required>
|
|
152
|
+
<Required htmlFor={ id(names.phone) }>{ t('routes_order.step4.phone') }</Required>
|
|
134
153
|
|
|
135
154
|
<input
|
|
136
155
|
type="text"
|
|
156
|
+
id={ id(names.phone) }
|
|
137
157
|
defaultValue={ values !== undefined ? values[names.phone] : '' }
|
|
138
158
|
{ ...refs(names.phone, Validate('required|min_length:5|max_length:50', t)) }
|
|
139
159
|
/>
|
|
@@ -156,10 +176,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
156
176
|
<div className="column">
|
|
157
177
|
{ has('whatsapp') && (
|
|
158
178
|
<div className="row">
|
|
159
|
-
<Required>{ t('routes_order.step4.whatsapp') }</Required>
|
|
179
|
+
<Required htmlFor={ id(names.whatsapp) }>{ t('routes_order.step4.whatsapp') }</Required>
|
|
160
180
|
|
|
161
181
|
<input
|
|
162
182
|
type="text"
|
|
183
|
+
id={ id(names.whatsapp) }
|
|
163
184
|
defaultValue={ values !== undefined ? values[names.whatsapp] : '' }
|
|
164
185
|
{ ...refs(names.whatsapp, Validate('required|min_length:5|max_length:50', t)) }
|
|
165
186
|
/>
|
|
@@ -170,10 +191,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
170
191
|
|
|
171
192
|
{ has('telegram') && (
|
|
172
193
|
<div className="row">
|
|
173
|
-
<Required>{ t('routes_order.step4.telegram') }</Required>
|
|
194
|
+
<Required htmlFor={ id(names.telegram) }>{ t('routes_order.step4.telegram') }</Required>
|
|
174
195
|
|
|
175
196
|
<input
|
|
176
197
|
type="text"
|
|
198
|
+
id={ id(names.telegram) }
|
|
177
199
|
defaultValue={ values !== undefined ? values[names.telegram] : '' }
|
|
178
200
|
{ ...refs(names.telegram, Validate('required|min_length:2|max_length:100', t)) }
|
|
179
201
|
/>
|
package/Step5/Addons/Addons.tsx
CHANGED
|
@@ -85,9 +85,15 @@ const Addons = ({ data, flexOffer, currency, whatsapp, whatsappNumber, telegram,
|
|
|
85
85
|
|
|
86
86
|
<Notice>{ t('routes_order.step5.addons.whatsapp.description') }</Notice>
|
|
87
87
|
|
|
88
|
+
{ /* Edited: Claude - Date: 2026-08-19
|
|
89
|
+
The number fields below have no visible label of their own -
|
|
90
|
+
they appear under the checkbox they belong to - so the
|
|
91
|
+
placeholder wording is repeated as an accessible name rather
|
|
92
|
+
than leaving them unnamed. */ }
|
|
88
93
|
{ whatsapp && (
|
|
89
94
|
<input
|
|
90
95
|
type="text"
|
|
96
|
+
aria-label={ t('routes_order.step5.addons.whatsapp.phone_placeholder') }
|
|
91
97
|
placeholder={ t('routes_order.step5.addons.whatsapp.phone_placeholder') }
|
|
92
98
|
value={ whatsappNumber }
|
|
93
99
|
onChange={ (event) => onChangeWhatsappNumber(event.target.value) }
|
|
@@ -143,6 +149,7 @@ const Addons = ({ data, flexOffer, currency, whatsapp, whatsappNumber, telegram,
|
|
|
143
149
|
{ sms && (
|
|
144
150
|
<input
|
|
145
151
|
type="text"
|
|
152
|
+
aria-label={ t('routes_order.step5.addons.sms.phone_placeholder') }
|
|
146
153
|
placeholder={ t('routes_order.step5.addons.sms.phone_placeholder') }
|
|
147
154
|
value={ smsNumber }
|
|
148
155
|
onChange={ (event) => onChangeSmsNumber(event.target.value) }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useState, useId } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
3
|
import { Required } from '@autobusal/common';
|
|
4
4
|
import { UseFormRegister, FieldErrors } from 'react-hook-form';
|
|
@@ -20,6 +20,18 @@ interface Props {
|
|
|
20
20
|
const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
21
21
|
const [ company, setCompany ] = useState<boolean>(false);
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Edited: Claude - Date: 2026-08-19
|
|
25
|
+
* Same fix as the passenger block: the labels here were text sitting
|
|
26
|
+
* beside the field rather than attached to it, so the billing form - the
|
|
27
|
+
* last thing between a buyer and paying - announced as unnamed edit
|
|
28
|
+
* boxes. Prefixed with useId rather than using the field name directly,
|
|
29
|
+
* because ids are page-global and these names are not guaranteed to be.
|
|
30
|
+
*/
|
|
31
|
+
const uid = useId();
|
|
32
|
+
|
|
33
|
+
const id = (field: string): string => `${ uid }${ field }`;
|
|
34
|
+
|
|
23
35
|
const countries = useCountries('all');
|
|
24
36
|
|
|
25
37
|
if (countries.isLoading) {
|
|
@@ -43,17 +55,17 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
43
55
|
|
|
44
56
|
<div className="column">
|
|
45
57
|
<div className="row">
|
|
46
|
-
<Required>{ t('routes_order.step5.billing.first_name') }</Required>
|
|
58
|
+
<Required htmlFor={ id('bill_fn') }>{ t('routes_order.step5.billing.first_name') }</Required>
|
|
47
59
|
|
|
48
|
-
<input type="text" { ...refs('bill_fn', Validate('required|min_length:2|max_length:100', t)) } />
|
|
60
|
+
<input type="text" id={ id('bill_fn') } { ...refs('bill_fn', Validate('required|min_length:2|max_length:100', t)) } />
|
|
49
61
|
|
|
50
62
|
{ Display(errors.bill_fn) }
|
|
51
63
|
</div>
|
|
52
64
|
|
|
53
65
|
<div className="row">
|
|
54
|
-
<Required>{ t('routes_order.step5.billing.last_name') }</Required>
|
|
66
|
+
<Required htmlFor={ id('bill_ln') }>{ t('routes_order.step5.billing.last_name') }</Required>
|
|
55
67
|
|
|
56
|
-
<input type="text" { ...refs('bill_ln', Validate('required|min_length:2|max_length:100', t)) } />
|
|
68
|
+
<input type="text" id={ id('bill_ln') } { ...refs('bill_ln', Validate('required|min_length:2|max_length:100', t)) } />
|
|
57
69
|
|
|
58
70
|
{ Display(errors.bill_ln) }
|
|
59
71
|
</div>
|
|
@@ -61,9 +73,9 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
61
73
|
|
|
62
74
|
<div className="column">
|
|
63
75
|
<div className="row">
|
|
64
|
-
<Required>{ t('routes_order.step5.billing.email') }</Required>
|
|
65
|
-
|
|
66
|
-
<input type="email" { ...refs('email', Validate('required|email|max_length:255', t)) } />
|
|
76
|
+
<Required htmlFor={ id('email') }>{ t('routes_order.step5.billing.email') }</Required>
|
|
77
|
+
|
|
78
|
+
<input type="email" id={ id('email') } { ...refs('email', Validate('required|email|max_length:255', t)) } />
|
|
67
79
|
|
|
68
80
|
{ Display(errors.email) }
|
|
69
81
|
|
|
@@ -73,35 +85,35 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
73
85
|
</div>
|
|
74
86
|
|
|
75
87
|
<div className="row">
|
|
76
|
-
<Required>{ t('routes_order.step5.billing.phone') }</Required>
|
|
88
|
+
<Required htmlFor={ id('bill_phone') }>{ t('routes_order.step5.billing.phone') }</Required>
|
|
77
89
|
|
|
78
|
-
<input type="text" { ...refs('bill_phone', Validate('required|min_length:10|max_length:20', t)) } />
|
|
90
|
+
<input type="text" id={ id('bill_phone') } { ...refs('bill_phone', Validate('required|min_length:10|max_length:20', t)) } />
|
|
79
91
|
|
|
80
92
|
{ Display(errors.bill_phone) }
|
|
81
93
|
</div>
|
|
82
94
|
</div>
|
|
83
95
|
|
|
84
96
|
<div className="row">
|
|
85
|
-
<Required>{ t('routes_order.step5.billing.address') }</Required>
|
|
97
|
+
<Required htmlFor={ id('bill_address') }>{ t('routes_order.step5.billing.address') }</Required>
|
|
86
98
|
|
|
87
|
-
<textarea { ...refs('bill_address', Validate('required|min_length:2', t)) }></textarea>
|
|
99
|
+
<textarea id={ id('bill_address') } { ...refs('bill_address', Validate('required|min_length:2', t)) }></textarea>
|
|
88
100
|
|
|
89
101
|
{ Display(errors.bill_address) }
|
|
90
102
|
</div>
|
|
91
103
|
|
|
92
104
|
<div className="column">
|
|
93
105
|
<div className="row">
|
|
94
|
-
<Required>{ t('routes_order.step5.billing.city') }</Required>
|
|
106
|
+
<Required htmlFor={ id('bill_city') }>{ t('routes_order.step5.billing.city') }</Required>
|
|
95
107
|
|
|
96
|
-
<input type="text" { ...refs('bill_city', Validate('required|min_length:2|max_length:100', t)) } />
|
|
108
|
+
<input type="text" id={ id('bill_city') } { ...refs('bill_city', Validate('required|min_length:2|max_length:100', t)) } />
|
|
97
109
|
|
|
98
110
|
{ Display(errors.bill_city) }
|
|
99
111
|
</div>
|
|
100
112
|
|
|
101
113
|
<div className="row">
|
|
102
|
-
<Required>{ t('routes_order.step5.billing.country') }</Required>
|
|
114
|
+
<Required htmlFor={ id('bill_country') }>{ t('routes_order.step5.billing.country') }</Required>
|
|
103
115
|
|
|
104
|
-
<select { ...refs('bill_country', Validate('required|min:1', t)) }>
|
|
116
|
+
<select id={ id('bill_country') } { ...refs('bill_country', Validate('required|min:1', t)) }>
|
|
105
117
|
<option value={ 0 }>{ t('routes_order.step5.billing.country_choose') }</option>
|
|
106
118
|
{ items }
|
|
107
119
|
</select>
|
|
@@ -119,17 +131,19 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
119
131
|
{ company && (
|
|
120
132
|
<div className="column">
|
|
121
133
|
<div className="row">
|
|
122
|
-
{
|
|
134
|
+
{ /* not Required - these are optional, so a plain <label>
|
|
135
|
+
rather than the asterisked one, but still associated */ }
|
|
136
|
+
<label htmlFor={ id('bill_company') }>{ t('routes_order.step5.billing.company') }</label>
|
|
123
137
|
|
|
124
|
-
<input type="text" { ...refs('bill_company') } />
|
|
138
|
+
<input type="text" id={ id('bill_company') } { ...refs('bill_company') } />
|
|
125
139
|
|
|
126
140
|
{ Display(errors.bill_company) }
|
|
127
141
|
</div>
|
|
128
142
|
|
|
129
143
|
<div className="row">
|
|
130
|
-
{ t('routes_order.step5.billing.nipt') }
|
|
144
|
+
<label htmlFor={ id('bill_nipt') }>{ t('routes_order.step5.billing.nipt') }</label>
|
|
131
145
|
|
|
132
|
-
<input type="text" { ...refs('bill_nipt') } />
|
|
146
|
+
<input type="text" id={ id('bill_nipt') } { ...refs('bill_nipt') } />
|
|
133
147
|
|
|
134
148
|
{ Display(errors.bill_nipt) }
|
|
135
149
|
</div>
|
|
@@ -137,9 +151,9 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
137
151
|
) }
|
|
138
152
|
|
|
139
153
|
<div className="row">
|
|
140
|
-
{ t('routes_order.step5.billing.comments') }
|
|
154
|
+
<label htmlFor={ id('comments') }>{ t('routes_order.step5.billing.comments') }</label>
|
|
141
155
|
|
|
142
|
-
<textarea { ...refs('comments') }></textarea>
|
|
156
|
+
<textarea id={ id('comments') } { ...refs('comments') }></textarea>
|
|
143
157
|
</div>
|
|
144
158
|
|
|
145
159
|
<div className="row row-nomargin">
|
|
@@ -60,7 +60,11 @@ const Coupons = ({ current, step2, t, total, onApplied }: Props): (JSX.Element |
|
|
|
60
60
|
<form onSubmit={ handleSubmit(onSubmit) }>
|
|
61
61
|
<ContainerForm>
|
|
62
62
|
<ContainerInput>
|
|
63
|
-
|
|
63
|
+
{ /* Edited: Claude - Date: 2026-08-19
|
|
64
|
+
aria-label rather than a <label for>, because this field has
|
|
65
|
+
no visible label to attach - the placeholder is the only
|
|
66
|
+
wording, and a placeholder disappears the moment you type. */ }
|
|
67
|
+
<input type="text" aria-label={ t('routes_order.step5.coupon.code') } placeholder={ t('routes_order.step5.coupon.code') } { ...register('code', Validate('required|min_length:4', t)) } />
|
|
64
68
|
|
|
65
69
|
<Apply
|
|
66
70
|
type="submit"
|