Skip to content

feat(field): add the postcode field and its React wrapper - #6

Merged
MobileMage merged 24 commits into
mainfrom
feat/field
Oct 4, 2026
Merged

MobileMage merged 24 commits into
mainfrom
feat/field

Conversation

@MobileMage

@MobileMage MobileMage commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

This adds @gatepost/field: a postcode input for plain HTML forms, as
the custom element gatepost-postcode-field. It reads a NIPOST postcode
as the user types, sends the canonical code with the form, works with
the keyboard alone, and shows each message once to a screen reader.

With a publishable key, the field checks each whole postcode with the
gateway and can fill in the postcode of the user's location. It sends
no request without a key, refuses a secret key, never shows a house
address, and never lets a failed lookup block the form. Without a key
it checks the format offline.

@gatepost/react wraps the element for React 18 and 19. It renders on a
server, passes each prop as an attribute, and calls the newest handler
once for each change.

The element is a plain custom element with no third-party runtime
dependency, so it cannot clash with another library on the page. Both
bundles stay under 10 kB against the 20 kB limit.

Component tests run in Chromium with scripted requests, and journeys
run the built element in a plain page against the mock server in
Chromium, Firefox and WebKit. No test calls NIPOST.

This change is larger than the 400 lines that GIT-5 suggests, because
the element's parts pass their tests only together. The repo also
moves to spec 0.3.0, which defines the field.

Signed-off-by: MobileMage aidelojejoshua@gmail.com

The spec now holds the contract of the field and its message
catalogue, which the next packages build on. The core and the client
behave as before, so each one only reports the new spec version. The
agents file names the new interface file, so an agent that needs a
field symbol finds where the spec lists it.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The field package starts with the parts that need no element yet. It
reads what a user typed with the core, and picks the message that the
spec names for each reading. The English messages come from the spec
catalogue through a generator, so a change of wording happens in one
place for every field. The tests run in Chromium from the start,
because the element that comes next needs a real browser.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A form needs a postcode input that sends the canonical code, works
with the keyboard alone and tells the user what is wrong in plain
words. The element takes part in the form through its internals, so a
form with no script logic submits it. It waits until the user leaves
the input before it shows an error, and it never applies a fix by
itself. axe-core checks each state in the browser, so it joins the
licence exceptions as a dev-only tool.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A placeholder named like an inherited property, such as toString,
inserted native function text instead of staying in view. fill now
reads only own properties. The count of a long paste now stops at the
input limit before it splits the whole text, and a segment error for a
segment that the grammar never fails now throws instead of showing the
unit message. The generator prints its problem without a stack trace,
rejects an empty catalogue and treats a missing target as out of date.
New tests cover quote and backslash escapes, an astral character,
code points at the limit and full-width digits.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Enter now follows the implicit submission of a native text input. The
form gets the name and value of its default button, a disabled default
button stops the submit, and a form with no submit button and a second
text field does not submit. The view writes a text only when it
differs, so a screen reader hears each new message once. The input
carries aria-required when the field is required. The change source
comes from the input event itself, a blur of the window no longer
shows an error, and a restored text counts as edited. The public type
names HTMLElement as its base, with no internal base class.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A page with a publishable key can now show the user that NIPOST knows
the postcode, and where it is. The field sends nothing without a key,
and it refuses a secret key, because anyone can read a key in a page.
A failed or negative answer never blocks the form, because the
gateway's data is new and the server checks the postcode again. The
field remembers only a lookup that the gateway answered, so a failed
lookup is tried again and a settled one is not sent twice. The house
address of a level 2 answer never shows, because a user can type any
postcode.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A submit button inside a disabled fieldset reads as enabled through its
disabled property, so Enter in the field submitted the form. A native
input does nothing there, because the button is disabled by its
fieldset. The field now asks the browser with the :disabled selector.
The paste test also covers the minus sign, which the core already maps
to a hyphen.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Many users do not know their new postcode yet, so the field can ask
the device where it is and the gateway which unit is there. It asks
only when the user presses its button. A fix that is not precise
enough for a building fills in only the first segments, and the user
types the rest, so the field never guesses a wrong unit. A refused or
missing location leaves the input ready for typing. A change of a
setting never cancels the request, and the gateway call uses the key
that the field holds when the position arrives. The field never shows
the address of the unit that the gateway found.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A base-url without a scheme, or a client that threw a plain error, left
the field in the state checking or GPS locating with no request behind
it, and a lookup kept that way blocked every retry. Any failure of a
lookup now shows the error state with the check failed message, and the
field does not remember the lookup. Any failure of the device call or
of reverse ends the request the same way. Only an error of the client
has a code, so only it raises gatepost-error.

The secret key error now comes after the render and the change event of
the same keystroke. The tests also cover the late answer of a cancelled
lookup, a base-url change that must not retry, and fetch spies that can
no longer reach the real gateway.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A lookup or a reverse call that throws an error of another kind, such
as a base address with no scheme, now raises gatepost-error with the
code network_error, as the failure table of the spec says. The state
and the message stay the same. The spec moves to the commit that adds
the two rows.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A device that rejects with a plain object, or whose location call
throws, now ends the request with the message and the event of a
refusal or of a missing location, so the field never stays on
"Finding your location". The change event of a coarse fill now comes
after the field shows its state. The rules for when a change keeps or
cancels a request moved to their own class. The tests wait for the end
state, and a test that sends a request without scripting it fails.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The component tests script the network, so they cannot show that a
real page, a real form and the mock server work together. The
journeys load the built one-file element into a plain HTML form,
submit it with the keyboard, and run axe in each state. They also
cover Enter with a disabled default button and the window blur that
reads the active element. pnpm check runs them in Chromium, and CI
runs them in Firefox and WebKit, the engine of Safari. The README
and its examples run in the same way. The README says that the first
alpha is not on npm yet, and that a property set before the element
is defined is lost.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A press of the location button left the lookup in progress set. If the
user then rewrote the text to the same postcode in another form, the
change matched that lookup and kept the location request, so the
position later overwrote the input. A location request now forgets the
lookup in progress when it starts, and the method that cancels on a
mismatch says so in its name.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A framework can render the field before the script that defines it
loads. A property such as messages then sits on the element itself
and hides the accessor, so the first value and every later write
never reached the field. The element now removes such an own
property when it connects, and sets it again through the accessor.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Every journey now runs under a fixture that stops a request to any
host except the test page and the mock server, and fails the journey
that made it. The secret-key journey compares the exact console
message, and the test page has an empty icon, so no engine logs a
404. The theming journey reads the outline of the field's own input.
New journeys cover the back button, paste, a slow lookup, the
suggestion button and the full-width row at 320 pixels. The README
now says that early properties work. The browsers job keeps the
trace of a failure.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
React apps, and the Next.js docs site, need the field as a component
with props and callbacks. The component renders the element with one
attribute for each prop, so a server render already holds the
settings, and it adds the listeners after the element mounts. It
passes only text to the element, because React 18 writes false as
text and the attribute would stay. The tests run on React 18 and 19,
and one test sets the messages before the element is defined.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
React 19 sets a prop of a custom element as a property when the
element has one of that name. So defaultValue reached the field as
the current text: a form reset emptied the field, and a new
defaultValue replaced what the user typed. The component now writes
defaultValue as the value attribute after the render, and the prop
keeps its first value. It also resets the messages when the prop goes
away, sets an unchanged messages object once, passes on the cleanup of
a callback ref, and drops the directive from the source file, because
the build banner writes it at the top of the built file.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
After the blur reformat, a later change of a setting read the new text
as a change by the user and cancelled a location request or cleared
its message. The key now loses the white space around it before the
field uses it, so a secret key with a space or a line break before it
is refused as any secret key is. The shadow tree is built with DOM
calls, so a page with a Trusted Types policy can show it. A message
that is not text keeps its English text. The README says that the
default colours are for a light page, gives a dark example, and
warns about the recent house address. The change files now describe
one release.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A page in plain JavaScript can set messages to null or undefined, and
the field now treats that as no messages, as the old spread did. The
dark-page test reads the style rules from the README, runs axe in the
quiet, valid, legacy and suggestion states, and has a control: the
same page with no tokens must break the colour contrast. The README
says that a dark page sets the seven colour tokens.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
@MobileMage MobileMage changed the title feat(field): add the postcode field for plain forms feat(field): add the postcode field and its React wrapper Oct 3, 2026
Keep the restyled READMEs of main for the root, core and client, with the
spec version 0.3.0 that this branch moves to. Move the Playwright set-up
steps of the root README into docs/develop.md, where main moved the
maintainer guide. Keep the spec submodule at the branch commit.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Add both packages to the package table. Both are alpha and not on npm yet.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Give the field README the logo, the name, the tagline, the badges and
the link row of the other packages, with the notice after them. Move
the sections into the same order, point questions to Discussions, and
link the maintainer guide. Keep the four HTML examples and the CSS
block that the tests read.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Give the React README the logo, the name, the tagline, the badges and
the link row of the other packages, with the notice after them. Move
the sections into the same order, point questions to Discussions, and
link the maintainer guide. Keep the quickstart that the test runs.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The field's spec and the WordPress floors are merged in the spec repo,
so the submodule now points at that commit on its main branch. The
glossary gains the check status term, copied from the spec as the
check asks.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
@MobileMage
MobileMage marked this pull request as ready for review October 4, 2026 00:57
@MobileMage
MobileMage merged commit 0003151 into main Oct 4, 2026
11 checks passed
@MobileMage
MobileMage deleted the feat/field branch October 4, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant