feat(field): generalize floating label to composed specialized fields

The floated-label fill test targeted `[data-field-input]` — the base
Field.Input only — so `<Field floatingLabel>` didn't float when wrapping a
specialized field (MaskField/NumberField/PasswordField/… render their own
`<input>`). Widen it to any non-hidden text input / textarea inside:
`:has(:is(input, textarea):not([type='hidden']):not(:placeholder-shown))`.

Now the floating label works for any composed text field (the vertical anchor
already aligns via shared --size-*-control-height tokens). Segmented controls
(Date/Time, contenteditable) still float on focus only — deferred.

Demo: MaskField floating example added to its Field-integration section.
Verified in browser: resting placeholder centred, floats onto the border when
filled, mask still conforms the value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 92f9723388
commit 7b3dc4d335

@ -327,9 +327,14 @@
inline-size: 100%;
}
/* Floated: focused OR filled → sit on the top border, shrunk. */
/* Floated: focused OR filled → sit on the top border, shrunk. The filled test
matches ANY non-hidden text input / textarea inside — so it works both for the
base Field.Input and for a specialized field composed in the slot (MaskField,
NumberField, PasswordField, SearchField, Textarea…), each of which renders its
own `<input>`. Segmented controls (Date/Time) use contenteditable, not
`:placeholder-shown`, so they float on focus only (not covered — deferred). */
[data-field][data-floating-label]:focus-within > [data-field-label],
[data-field][data-floating-label]:has([data-field-input]:not(:placeholder-shown)) > [data-field-label] {
[data-field][data-floating-label]:has(:is(input, textarea):not([type='hidden']):not(:placeholder-shown)) > [data-field-label] {
/* `--field-floating-label-top` (default 0) is the floated centre line — 0 sits
on the control's top border (outlined). Themeable without touching this rule. */
inset-block-start: var(--field-floating-label-top);

@ -37,6 +37,7 @@
let invalid = $state(false);
// ── Field integration ────────────────────────────────────────────────
let floatValue = $state('');
let fieldDisabled = $state(false);
let fieldReadonly = $state(false);
let fieldRequired = $state(false);
@ -372,6 +373,20 @@
</Field>
</div>
<p data-uix-section-desc style="margin-block-start: var(--space-4)">
<code>&lt;Field floatingLabel&gt;</code> — the floating label works with a composed MaskField
(the input carries <code>placeholder=" "</code> so its empty state is detectable).
</p>
<div style="max-inline-size: 22rem; margin-block-start: var(--space-2);">
<Field floatingLabel>
<Field.Label>Phone number</Field.Label>
<MaskField mask="(999) 999-9999" bind:value={floatValue}>
<MaskField.Input placeholder=" " />
</MaskField>
<Field.HelperText>Format: (999) 999-9999</Field.HelperText>
</Field>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
</div>

Loading…
Cancel
Save

Powered by TurnKey Linux.