feat(webkit): [ENG-47059] add the password-requirements row with default rules - #848
Open
herbert-julio-azion wants to merge 11 commits into
Open
feat(webkit): [ENG-47059] add the password-requirements row with default rules#848herbert-julio-azion wants to merge 11 commits into
herbert-julio-azion wants to merge 11 commits into
Conversation
herbert-julio-azion
marked this pull request as ready for review
August 3, 2026 22:04
…assword from Figma Implements the four Figma frames of the password evolution. Three of them (_Password Requirement Chip, _Must Contain, _Score Password) are the internals of one surface — a captioned, wrapping set of rule chips — and the fourth (FieldPassword, states Default/Error/Required) is where that surface lives. None of the four is the bare InputPassword, so the row lands on FieldPassword, which the InputPassword spec already designates as the owner of strength meters. FieldPassword gains two props: `requirements` (bulk data, one chip per entry, `validated` switching the chip to the check glyph + success tokens) and `requirementsTitle` (the caption, which also names the group for assistive tech via role=group + aria-labelledby). Evaluating the rules stays in the consuming application; the field renders the outcome. Reconciles InputPassword against the frame as it renders inside FieldPassword: trailing padding 4px -> var(--spacing-xs), and the visibility toggle from the 32px `medium` IconButton to the 28px `small` one the frame specifies. The frame's 0.8px var(--border-width-default) is deliberately NOT adopted — the spec pins InputPassword to InputText's borders and InputText is still on 1px, so it is recorded as a theme gap for one coordinated PR across every input. Also fixes a latent id bug found while verifying the render in a real browser: `inputId` defaults to '', which is not nullish, so `props.inputId ?? generatedId` never fell back to useId(). Every derived id collapsed to a bare suffix, the label/aria-describedby associations silently dangled, and two id-less fields on one page emitted duplicate ids. `||` restores the fallback the spec already promised. Both specs updated first (props, states, motion, tokens, a11y, stories) with recomputed checksums; the FieldPassword story documents the new axis in one Requirements story; 10 new browser-mode tests cover the row, the group naming and the id fallback. NOTE: the visual-regression baselines for InputPassword and FieldPassword need regenerating in CI (/regen-baselines) — the toggle size, trailing padding and the new row all change those snapshots.
…ue and animate the chips The rule entries carried a pre-computed `validated` boolean, so the chips froze at whatever the consumer passed and typing changed nothing. An entry now carries its `test` (a RegExp or a predicate) and the field evaluates it against the value it already owns, so the row tracks what is typed with no recomputation on the consumer side. Regexes with the `g` flag are rebuilt without it, since `lastIndex` would make `test()` alternate on the same value. Satisfying a rule inserts its check glyph, which widens that chip and pushes the ones after it along the row. That is a layout move, so `move-class` (Vue's FLIP hook) animates the shift with `transform`; entering and leaving chips slide in on the same axis. The row itself transitions height and width, with `interpolate-size: allow-keywords` so the transition reaches `auto`. Three regexes written inside JS string literals emitted `/d/` instead of `/\\d/`, which the "Show code" snippet and one story field were using verbatim.
…mponent with layout-safe motion The previous motion animated size, which is the one thing it must not do: the row transitioned `height`/`width` (with `interpolate-size`) and the chips relied on TransitionGroup's FLIP because mounting the check glyph on satisfaction widened a chip and pushed the rest of the row along, so a line break could move mid-transition. The leaving chip was taken out of flow with `absolute`, which shifted the row again. The glyph is now always in flow inside a reserved fixed box, so a chip is the same width whether its rule is met or not, and only `opacity` and `transform` animate: both are composited and neither takes part in layout, so no motion can change a size or re-wrap the row. The height/width transition and TransitionGroup are gone. The row moves into an internal `field-password-requirements` sub-component, mounted only when the `requirements` prop is non-empty. It owns the rule evaluation and declares `PasswordRequirement`, which FieldPassword re-exports so the public type is unchanged. FieldPassword stays monolithic; the consumer drives the row through the prop.
…d a clickable input edge Three changes, all reported from review. The chip's icon box was blank for unmet rules, which read as a gap before the label. Every state now carries a glyph (met / unmet / failed), so the box is never empty and the chip still never changes width: only opacity and transform animate. The row gains the error surface. Setting `invalid` moves every unmet rule to `failed` (danger tokens + X glyph) while satisfied rules stay met, so after a blocked submit the reader sees which rules failed. All three glyphs are props, so the set is swappable. The input's left padding lived on the container, so the 16px strip before the text was not part of the input and clicking it did not focus the field. `input-number` pads the input itself, which is why the other inputs behave. The padding moves onto the input, with `group-data-[has-icon-left]` keeping the icon-left layout pixel-identical.
…tate and per rule Nothing shows while a rule is being typed: `requirementsPendingIcon` defaults to empty and the glyph is rendered only when its state resolves to one, so there is no reserved box and no gap before the label. The chip grows when a glyph arrives, which is the accepted trade. Customisation now sits on two levels. The `requirements*Icon` props set the row default; any entry of the validation object overrides its own `icon` / `pendingIcon` / `invalidIcon`. An empty string means render none, resolved with `??` so `''` is honoured instead of falling back. A consumer that wants no X and no red simply leaves `invalid` unset, or empties the glyph it does not want.
…input width Mounting the glyph box on demand changed the chip's width, and any ancestor sized by its content passed that to the input: measured 353.4px to 368.0px in an auto-width host as the rules started passing. The 14px box is now always reserved and only the glyph inside it is conditional, which holds the chip at a constant width; measured 368.0px to 368.0px. Two attempts on the row itself did not work and are recorded so they are not retried: `w-0 min-w-full` (the cyclic min-width is resolved from the content, so the row still fed the ancestor) and `contain: inline-size`. The cost of the fix is a small gap before the label while a rule shows no glyph, which is the price of the guarantee.
… and no width change Both constraints hold at once now. The glyph is conditional again, so a rule that shows nothing reserves no box and leaves no gap before the label, and the row declares `contain: inline-size` so its contents never feed an ancestor's intrinsic width. Measured on the input in an auto-width host: 353.4px to 368.0px without containment, 203.0px to 203.0px with it. The consequence is deliberate: in an auto-width host the field is now sized by its label and input rather than stretched by the rule chips, so it is narrower and the chips wrap within it. In a definite-width container nothing changes. `w-0 min-w-full` was tried first and does not work: the cyclic min-width is resolved from the content, so the row kept feeding the ancestor. An earlier always-reserved box worked for the width but reintroduced the leading gap.
…ree of empty space `contain: inline-size` kept the input width constant but changed the field's resting width in an auto-width host (353.4px to 203.0px), which is a width change of its own. Reverted: nothing in the row touches the field's width mechanics now. The two guarantees hold together in a field with a definite width, which is what a form gives it: measured the input at 274.0px through the whole transition with no empty box in any chip. In an auto-width host the field's width is derived from the row's content, so the chip's growth reaches it (353.4px to 368.0px); a width on the field is what resolves it.
…rement chips Read from Figma node 4770:31936 (_Must Contain) this session: the chip has exactly two states. Not satisfied is var(--bg-surface-raised) with var(--text-muted) and no glyph at all; satisfied is var(--success) with a 14px pi-check and var(--text-default). There is no error chip in the design: in the frame's Error state only the input border and the HelperText turn red, and the chips are unchanged. So the failed state, its X glyph and its danger tokens were invented here, and they go, along with the row's invalid and invalidIcon props, the per-rule invalidIcon override, FieldPassword's requirementsInvalidIcon, the forwarding of the field's invalid into the row, the RequirementsInvalid story and the tests that asserted that state. data-validated is now the single switch; data-state is gone. The field's own invalid prop is untouched: it drives the input border and the HelperText, which IS in the design. Tail cleaned up in the same pass: a comment and two spec passages still described the reserved-glyph-box design that no longer exists, the animation table promised a transform transition no state varies (now transition-opacity only), the token table credited the glyph with var(--success-contrast) when it inherits the chip's var(--text-default), and one test asserted the absence of the now-nonexistent data-state. Added the coverage a verifier found missing: the g-flag RegExp guard and both branches of a predicate test.
…ery appearance knob The requirements row keeps exactly two capabilities: it is enabled by a prop, and the consumer may supply their own rules. Everything about how it looks is fixed from Figma node 4770:31936 — two chip treatments, unsatisfied being the muted surface with no glyph and satisfied the success surface with a pi-check. So the icon knobs are gone: the row's icon and pendingIcon, the per-rule overrides, and FieldPassword's requirementsIcon and requirementsPendingIcon. The five rules ship as data in a co-located password-requirements.ts with a stable key each, so a bare `requirements` renders the built-in set and an array replaces it. Both the Storybook docs and the spec explain using the defaults, replacing them, and composing from them. The Ctrl+V report was measured and is not a defect: driven with a real Playwright Ctrl+C/Ctrl+V, a 50-char paste turns all five chips green and a 300-char paste turns four green, leaving only '8-128 characters' grey — which is what the interval says. The naive probe (dispatching an untrusted ClipboardEvent) would have falsely confirmed the bug.
herbert-julio-azion
force-pushed
the
feat/ENG-47059-field-password-requirements
branch
from
August 4, 2026 13:55
d952b52 to
ed7cf0e
Compare
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.
Closes ENG-47059. Implements the password-requirements row from Figma
(
FieldPassword2027:3212,_Must Contain4770:31936,_Password Requirement Chip4770:31930,
_Score Password4770:31947).What the field gains
A captioned, wrapping row of rule chips under the input, enabled by prop. It keeps exactly
two capabilities and nothing else: it is enabled by a prop, and the consumer may
supply their own rules. Everything about how it looks is fixed from the design.
A bare
requirementsrenders the built-in set; an array replaces it entirely;false(thedefault) renders no row. Each built-in rule carries a stable
keyso a consumer can startfrom the set and drop one without depending on the localizable label.
The chip has two treatments, straight from the design: unsatisfied is
var(--bg-surface-raised)+var(--text-muted)with no glyph, satisfied isvar(--success)+ a 14pxpi-check+var(--text-default). The chip is wider oncesatisfied, which is exactly the 88px to 106px delta between the two Figma variants.
A rule carries its test, not its result: the field owns the value, so it evaluates every
rule against the current one and the chips track what is typed with no wiring on the
consumer side. A
g-flagged pattern is rebuilt without the flag, becauselastIndexwouldotherwise make the same value alternate.
Also in here
resolvedInputIdused??against a''default, souseId()neverfired and every derived id collapsed to a bare suffix, duplicated across instances with a
dangling
label forandaria-describedby. Now||, with a test that mounts two id-lessfields in one app and asserts distinct ids.
<input>, so the strip beforethe text is part of the field and clicking it focuses.
input-numberalready did this;input-passworddid not.group-data-[has-icon-left]keeps the icon-left layoutpixel-identical.
smallIconButton the frame specifies, and the trailingpadding is
var(--spacing-xs)per the reconciled frame.The Ctrl+V report: measured, not a defect
Pasting a large text was reported as not counting towards "8-128 characters". Driven with a
real Playwright Ctrl+C/Ctrl+V (OS-level,
isTrustedevents): a 50-char paste turns all fivechips green; a 300-char paste turns four green and leaves only
8-128 charactersgrey,which is what an inclusive 8..128 interval says. The other four lighting up is what makes
the single grey chip read as "the paste was not counted".
Worth recording: the naive probe (
dispatchEvent(new ClipboardEvent('paste', …))) yieldsisTrusted=falseand leavesvalue.lengthat 0, so measuring that way would have falselyconfirmed the bug.
If the intent is that a 300-char paste should end up valid, the fix is a
maxLengthof 128at the call site so the browser truncates; it is not a change to this component.
What a reviewer should weigh before merge
These are known and deliberate, not oversights:
The labels are the design's; the regexes are not in Figma.
azion-console-kitenforces(?=.*[-_!@#$%^&*(),.?":{}|<>])for the special character and a minimum of 8 with noceiling, in
src/templates/signup-block/login-with-email-block.vue. The shipped defaultuses
[^A-Za-z0-9], which is weaker: a plain space satisfies it. Aligning the specialset, and the
keynames (minLength/specialChar, which the console already uses insrc/views/YourSettings/FormFields/FormFieldsYourSettings.vue), is a follow-up../password-requirements), which deviates from thehouse precedent:
field-phone-number'scountries.tsis internal, reachable only throughthe prop default. The deviation buys composability (filtering the built-in set by
key);the precedent's trade is that overriding means passing the whole array.
requirementsis a union type (boolean | PasswordRequirement[]). No other componenthere uses a union prop; the house shape would be a boolean gate plus an overridable array,
as
calendar-periodandfield-phone-numberdo respectively.var(--success-contrast)(the design binds it; the code lets it inherit), the chip heightis fixed
20pxin the design and a floor (min-h-5) in the code, the chip label ismissing
whitespace-nowrap, and the pending-glyph axis is code-only and unreachable withthe shipped defaults.
must not be read as the password policy.
Verified
71 tests pass across
field-passwordandinput-password, lint clean,vue-tscclean, the89 story files comply with
storybook-source.md,check-testscovers 81 components, and thespec checksum matches.