refactor(web): network connection form UX and composition improvements (part 2) #3338
Open
dgdavid wants to merge 16 commits intoenhance-network-connection-formfrom
Open
refactor(web): network connection form UX and composition improvements (part 2) #3338dgdavid wants to merge 16 commits intoenhance-network-connection-formfrom
dgdavid wants to merge 16 commits intoenhance-network-connection-formfrom
Conversation
Introduces `ChoiceField`, a generic TanStack Form-aware select component for mode/behavior selection. Registered in `fieldComponents` so it is accessed as `field.ChoiceField` inside `form.AppField` render props. Splits form contexts into `hooks/form-contexts.tsx` to avoid the circular import that would arise from field components importing `useFieldContext` while `hooks/form.tsx` imports those same components for registration.
Adds Pattern 5 (choice selector) and renumbers the former patterns 2–5 to make room for it. Reorders patterns from least to most intrusive and rewrites the "Combining patterns" and "Choosing the right pattern" sections accordingly.
Renders a protocol-specific IP configuration block using a three-level structure: mode selector (Default/Custom), method selector (Automatic/Manual), and the corresponding fields. Manual mode shows required addresses plus optional gateway and DNS. Automatic mode offers an opt-in checkbox for extra static settings on top of DHCP. Uses `useFormContext` following the TanStack Form composition guide, so it can be dropped into any `useAppForm`-backed form without prop-drilling. Field names are passed explicitly via `fieldNames`.
Replace the flat IPv4/IPv6 method selectors, the combined IP Addresses textarea, and the top-level "Use custom DNS servers" checkbox with two IpSettings components, one per protocol. Each manages its own mode (Default/Custom), method (Automatic/Manual), addresses, gateway, nameservers, and the opt-in "With extra IPv4/IPv6 settings" toggle. Wrap the form body in form.AppForm so child components can access the form context via useFormContext. The submit handler merges per-protocol addresses and nameservers into the flat arrays expected by the Connection constructor, including values only from protocols with active custom configuration. All IpSettings field labels are prefixed with the protocol name (e.g. "IPv4 Gateway" instead of "Gateway") so each label is self-sufficient when announced by a screen reader navigating outside the visual grouping. Sighted users benefit too: the prefix removes any ambiguity when both protocols are visible at once. See WCAG 2.4.6 (Headings and Labels): https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels.html The gateway field in DHCP+extra mode carries the suffix "(optional, ignored without a static IP)" to clarify that a gateway alone has no effect without at least one static address alongside it.
Replace the two-level Default/Custom mode + Auto/Manual method + "With
extra settings" checkbox structure with a single three-option selector:
Default, Automatic, and Manual.
- IpSettings now takes only three field names: mode, addresses, gateway
- Addresses are optional in Automatic mode and required in Manual mode
- Gateway is shown for both non-default modes; omitted at submit time
unless at least one address is present
- DNS servers moved back to the top level in ConnectionForm as a shared
checkbox+textarea
Most users choosing Automatic (DHCP) do not need to set static addresses or a gateway alongside it. Showing those fields upfront adds visual noise and may confuse users who are not aware that combining DHCP with static settings is even a valid configuration. A "Show advanced settings" toggle now appears alongside the mode selector in Automatic mode, hiding the extra fields until explicitly requested. Manual mode is unaffected and always shows addresses and gateway, since those are the point of choosing it. The submit handler only collects addresses and gateway for Automatic mode when the advanced toggle is enabled.
…pproach 2) Previous iterations either showed static address fields for all Automatic users (too much upfront complexity) or hid them behind a checkbox toggle alongside the selector (two controls where one should suffice). Both violated the principle of progressive disclosure. This approach keeps a single selector but adds a fourth option, Mixed, which makes the uncommon DHCP+static combination an explicit choice rather than a hidden extra. Users who just want DHCP pick Automatic and see nothing else. Users who need static addresses on top of DHCP pick Mixed and get exactly that. The right complexity is revealed only when the user actively asks for it. Descriptions do the heavy lifting: instead of hiding complexity behind a toggle, they guide the user toward the right choice, including a "not needed for most setups" nudge on Mixed. Each option carries a protocol-aware description where relevant: Automatic shows DHCP for IPv4 and SLAAC or DHCPv6 for IPv6, reflecting how NetworkManager's auto method behaves differently per protocol.
Replace the plain FormSelect-based interface field with two dedicated ChoiceField components for interface binding UI: - BindingModeSelector: wraps the binding mode selection (Unbound / By device name / By MAC address) with descriptions that clarify each option's effect on the connection. - DeviceSelector: picks a network device by interface name or MAC address; the non-selected identifier is shown as a styled description for context. Both components use useFormContext internally and are laid out side-by-side via Flex/FlexItem in ConnectionForm, with DeviceSelector appearing only when the mode is not Unbound (via form.Subscribe).
The Default option is dropped from IP settings. It represented a NetworkManager implementation detail (no method written to the profile) that most users would not understand. Automatic now covers that common case, with NetworkManager deciding how to configure the interface. Mixed is renamed to Advanced DHCP (IPv4) or Advanced Automatic (IPv6), making it clear it extends the automatic case rather than a separate alternative. Option descriptions are rewritten to be concise and neutral: no trailing periods, no user-targeting language, no protocol jargon. "Interface binding" renamed to "Device" and its options drop technical vocabulary in favor of plain language: Any, Chosen by name, and Chosen by MAC. The device picker that appears next to the binding selector gets a visually hidden label, since the selector already provides enough context.
The IPv4 and IPv6 mode selectors used different labels for the same option: "Advanced DHCP" and "Advanced Automatic". The distinction was technically accurate: IPv4 automatic addressing uses DHCP, while IPv6 uses SLAAC, making "DHCP" wrong for IPv6. Hence "Advanced Automatic". However, such asymmetry could raise the exact question it tried to avoid: why does one selector say DHCP and the other say Automatic? Both words describe the underlying mechanism, not the outcome, conflicting with the plain-language description style adopted for all other options. This commit changes both labels to "Advanced". The description still conveys the relationship to Automatic without encoding protocol-specific implementation details in the label.
…-components IpSettings, BindingModeSelector, and DeviceSelector previously used useFormContext(), which is designed for generic leaf field components and is deliberately untyped. Since these components render slices of a known form, withForm() is the correct fit. The mismatch forced "as any" casts on every field name and subscribe selector, silently bypassing TypeScript's checks and leaving field renames undetected at compile time. Rewriting the three components with withForm() removes all "as any" casts and restores full type safety. The form options were extracted from ConnectionForm using formOptions() and exported to allow sub-components spread it in their withForm() definition for type inference. Also, a convenience mergeFormDefaults helper has been added for handling the case where some defaults depend on runtime hook values and cannot be declared statically.
And use <Text srOnly> directly instead. HiddenLabel implied a <label> element but just rendered <Text srOnly>.
FlexItem with no props adds no value. Flex lays out its direct children as flex items regardless.
dgdavid
commented
Mar 27, 2026
| */ | ||
| import { createFormHookContexts } from "@tanstack/react-form"; | ||
|
|
||
| export const { fieldContext, formContext, useFieldContext, useFormContext } = |
Contributor
Author
There was a problem hiding this comment.
useFormContext is exported but currently unused. It was originally the mechanism used by IpSettings, BindingModeSelector, and DeviceSelector to access the form instance, but was later replaced by withForm(). It is kept because it is the intended API for components registered in formComponents (e.g. a submit button that reacts to form state), which are not yet present but expected as the form layer grows.
Since the actually does not have JSX content.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR: Refine connection form for cleaner device and IP settings interface, with clearer option labels and a more logical field order.
This PR continues the reimplementation of the network connection form, built on top of the TanStack Form foundation introduced in part 1. It focuses on two areas: refining the IP settings and device binding UX, and improving the form architecture to align with TanStack Form's composition model.
What is included
IP settings redesign
The IP settings block is extracted into a standalone
IpSettingscomponent, rendered once per protocol. The settings selector offers three options: Automatic, Manual, and Advanced. Manual and Advanced reveal address and gateway fields while Automatic keeps the form clean. Option descriptions tries to use no user-targeting language as well as no implementation-detail jargon.Device binding redesign
The interface binding section is extracted into
BindingModeSelectorandDeviceSelectorcomponents. Options use plain language (Any, Chosen by name, Chosen by MAC) rather than technical terms as in the current (to be drop)BindingSettingsForm. DeviceSelector's label is visually hidden since the binding selector already provides visual context.TanStack Form composition
These components were first implemented making use of useFormContext(), which is deliberately untyped since it is designed for generic leaf field components where the form type is not known at definition time. But later rewritten to use
withForm()instead, since it gives each component a typed form instance, removes all "as any" casts, and catches field name errors at compile time.Two example of the form in action. The form has several other states worth exploring it by yourself. The easiest way is to check out the branch and play with it.
Keep in mind this is an intermediate step. Some things are intentionally simplified for now, such as the textarea inputs that will eventually become a more fluid multi-value experience.
For the reasoning behind field visibility and label choices, see conventions.md.
What is deferred to follow-up PRs
Same as part 1, still pending:
Type-specific subforms (bond, bridge, VLAN).Actually out of the scope of this serie of PRsNotes for reviewers
This PR took longer than expected due to four iterations of the form layout (still not fully finalized) before finding a version that balances an easy-to-understand workflow with the ability to configure complex connections.
Although it has almost the same pending items as the previous commit, it was opened as a separate PR rather than keep adding more and more changes.
The intermediate commits from the UI iterations have been preserved in the history. It does not hurt to have them there and may be useful if any past decisions need to be revisited. Therefore, it is recommended to focus on reviewing the final result rather than each commit individually. For details on the final layout reasoning, see the notes in #3337.
Finally, TSDoc blocks and code can still be improved or shortened in some places. However, documenting the first TanStack Form components thoroughly provides helpful context, as these components are new to the project and the documentation migth aid understanding.
Changelog entry postponed for the final PR from feature branch against master.
Related to https://trello.com/c/rUEeqOkf (protected link)