On the slot booking page, clicking a quantity stepper while the previous click's request is still out loses the new value: the field snaps back to the quantity the server computed before that click existed, and only the earlier quantity is held.

The cause is the shape of the answer, not the timing. The live update replied with the rebuilt form, so it replaced every quantity field on the page, including the one the visitor is using. A click made in flight was overwritten by its own answer.

To reproduce: open a slot booking page offering a tariff with places left, click plus, then click plus again before the first request completes. The field ends on 1, and one place is held.

The fix answers with state rather than markup. A new AJAX command carries, for each offer, the availability line, the ceiling, the sold-out flag and the quantity the server holds; the browser applies them to the cards already on the page. Nothing is rebuilt, so there is nothing to roll back, and an open panel and the keyboard focus stay where they were. A click the request could not carry is sent by that request's answer rather than racing it, and a quantity typed straight into a field is now held the same way a clicked one is.

Two accessibility consequences, both deliberate. A correction the visitor is not told about would be the worst outcome available, so where the server holds fewer places than were asked for, the card carries the reason in a live region under the control and the field points at it with aria-describedby. That region is rendered empty from the first paint rather than inserted when it has something to say, and it is never hidden with display: none, because a live region that gains text in either of those states is announced by nothing. The wording is the booker's own where it sent any, and otherwise names both numbers.

Refusals used to be queued as page-level messages, which render nowhere in an AJAX response and then surface on whatever page the visitor opens next, out of context and by then often untrue. On the live path they now travel with the offer they belong to.

Covered by four new browser tests in OfferStepperTest: a click made provably in flight (hung off jQuery ajaxSend rather than a sleep), the fields surviving as the same DOM elements, the availability line counting down, and a refused quantity being explained and linked from the field.

Issue fork yoyaku-3614348

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Status: Active » Needs review

  • mably committed 4b1556a1 on 1.x
    fix: #3614348 A stepper click made while its request is in flight is...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

mably’s picture

Merged to 1.x as 4b1556a. The answer to a stepper click now carries state rather than the rebuilt form: one command per request, holding each offer's availability line, its ceiling, its sold-out flag, the quantity the server holds and any refusal text. Nothing on the page is rebuilt, so a click made while a request is out is kept and sent by that request's answer, and the AJAX wrapper and the refocus workaround are both gone with the full replace that needed them.

Three further defects came out of testing it, all fixed here. A message arrives as rendered markup, so writing it into the region as text printed the placeholder tags on the page for the visitor to read. A number being typed was unprotected, because intent was recorded only when the field was left, so a request that happened to be out at the time came back and cleared the field; intent is now recorded on every keystroke, and a field the visitor has open in front of them is never written to unless the server is correcting what was posted for it. And what a request carried is now read from the fields rather than from the recorded intent, so a refusal of a value that arrived any other way is still reported instead of passing in silence.

Accessibility: each card renders an empty status region from the first paint, never hidden with display: none, because a live region that gains text in either state is announced by nothing; the field points at it with aria-describedby; and a corrected quantity is always explained, in the booker's own words where it sent any and otherwise naming both numbers.

The stepper's browser coverage went from sixteen tests to nine, grouped so a site install is not paid for every fact, with the checks collected and asserted once at the end so one failure does not hide the rest.

Two follow-ups were filed from this work: #3614349: Cap what one transaction may take of a tariff, and move the policy system to the engine for a per-cart selling limit, and #3614361: Stepping a quantity up places one seat at a time, so a party is spread around the venue for placement seating a party one seat at a time.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.