@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 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. Only on the route-choosing screens, and only
322
- once the URL names a real pair: this is the page a search engine
323
- lands on, and it is the one place the block adds anything. Past
324
- step 2 the reader has picked a coach and is filling in passenger
325
- details - facts about the pair are noise at that point.
326
-
327
- It renders nothing at all for a pair with no facts, so a page that
328
- would only have gained an empty box does not gain a box. */ }
329
- { (showStep2 || showStep3) && (
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
  />
@@ -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
- { t('routes_order.step5.billing.company') }
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
- <input type="text" placeholder={ t('routes_order.step5.coupon.code') } { ...register('code', Validate('required|min_length:4', t)) } />
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.31.2",
3
+ "version": "1.31.4",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"