Read-only and disabled are two states
Read-only keeps the value in the tab order and lets it be copied; disabled takes it out. Both are the input's native attributes, so the platform announces them itself.
A text field on the platform's own <input> element, wired to the label and the error around it.
@pacit/components/fieldinput[pctText]Semantics come from the native <input> — no role and no keyboard handling of ours
import { Component } from '@angular/core';
import { PctField, PctText } from '@pacit/components/field';
/** The plainest control there is — which is exactly the promise: a native input, styled. */
@Component({
selector: 'demo-text',
imports: [PctField, PctText],
styles: ':host { display: block; max-inline-size: 24rem; }',
template: `
<pct-field label="Workspace name" hint="Lowercase, dashes allowed">
<input pctText value="acme-design-system" />
</pct-field>
`,
})
export class TextDemo {}text.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctText, PctField } from '@pacit/components/field';| Exported | What it is |
|---|---|
PctFieldcomponentpct-field | The chrome of a form field: label, hint, warning, error message, the required marker and the [pctPrefix] / [pctSuffix] slots inside the field. |
PctFieldSizetypePctSize | The size of a field: the library's own scale. |
PctTextcomponentinput[pctText], textarea[pctText] | Text field: a directive on a native <input>. The input is not wrapped in a component of ours, so type, browser autofill, mobile keyboard modes and the whole semantics are kept without an abstraction in between (req-api-platform). |
PctAutosizedirectivetextarea[pctText][pctAutosize] | A <textarea> as tall as what is written in it — the ARIA APG has no pattern for this, because it is not a behaviour but a height. |
PctNumbercomponentinput[pctNumber] | Number field: a component on a native <input type="text"> with the spinbutton role, a value of type number | null and locale-aware formatting. |
PctAffixFittype'inset' | 'fill' | How a decoration sits in its slot: |
PctPrefixdirective[pctPrefix] | A decoration before the control, inside the field border (a currency unit, an icon). Decorative content should be hidden from the screen reader (aria-hidden), or have an accessible name of its own if it is interactive. |
PctSuffixdirective[pctSuffix] | A decoration after the control, inside the field border (a clear button, say). |
PctLabelAuxdirective[pctLabelAux] | A label add-on — content in the label row, aligned to the end (an "i" icon with a hint about the field, a help link). It lies outside the field border, so it does not interfere with the control's touch area. If it is interactive (a button, a link) it needs an accessible name of its own. |
PctMessageAuxdirective[pctMessageAux] | A message-line add-on — content in the row below the field, aligned to the end (a character counter, say). It shares the row with the hint or the error: the chrome shows only one of those messages below the field, and this slot stands beside it whichever one is lit. |
<pct-field label="Workspace name">
<input pctText />
</pct-field>Each example is the component itself, not a screenshot. Open the code under any of them.
Read-only keeps the value in the tab order and lets it be copied; disabled takes it out. Both are the input's native attributes, so the platform announces them itself.
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
unreleased marks 1 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.
Text field: a directive on a native <input>. The input is not wrapped in a component of ours, so type, browser autofill, mobile keyboard modes and the whole semantics are kept without an abstraction in between (req-api-platform).
| Name | Type | What it does |
|---|---|---|
valuetwo-waydefault '' | string | The value — a required field of the FormValueControl contract. |
disableddefault false | boolean | Blocks the control and greys it — the native disabled, so it leaves the tab order as well. With [formField] the directive writes it, as it writes every input of the FormUiControl contract. |
readonlydefault false | boolean | Keeps the value from being edited while the field stays focusable and readable — the native readonly. |
invaliddefault false | boolean | The form's verdict; shown only once touched, so an empty form does not open red. |
toucheddefault false | boolean | Whether the user has left the field once; with invalid it gates the error face. |
requireddefault false | boolean | The native required; a pct-field around the control reads it to mark the label. |
errorsdefault [] | readonly ValidationError.WithOptionalFieldTree[] | The form's validation errors; a pct-field around the control shows the first one's message in place of the hint once the field is touched. |
warningsunreleaseddefault undefined | readonly ValidationError.WithOptionalFieldTree[] | undefined | A verdict without a veto (0087): shown after the error and before the hint once the field is touched, in the warning tone, with aria-invalid untouched. Left unbound, the control reads what pctWarn() wrote for its own [formField]; bound — [] included — the list given stands. |
namedefault '' | string | The native name — what a form submission calls the value. |
| Name | Payload | When it fires |
|---|---|---|
touch | void | Emitted on blur — lets the form mark the field as touched. |
The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-text | |
idproperty | controlId | |
disabledproperty | disabled() | |
readOnlyproperty | readonly() | |
name | name() || null | |
required | required() || null | |
aria-invalid | showInvalid() ? "true" : null | |
aria-describedby | describedBy() |
Two ways in, both stable. No class names to depend on.
In a test, PctTextHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.
| Part | Selector | What it is |
|---|---|---|
field-control | [data-pct-part="field-control"] | the wrapped control itself |
field-error | [data-pct-part="field-error"] | the message when the control is invalid |
field-footer | [data-pct-part="field-footer"] | the row under the control: the hint, the warning or the error |
field-header | [data-pct-part="field-header"] | the row above the control: the label and what stands beside it |
field-hint | [data-pct-part="field-hint"] | the hint, read as the description of the control |
field-label | [data-pct-part="field-label"] | the label, tied to the control by id |
field-label-aux | [data-pct-part="field-label-aux"] | the slot beside the label — a counter, a link |
field-message-aux | [data-pct-part="field-message-aux"] | the slot beside the hint, the warning or the error |
field-prefix | [data-pct-part="field-prefix"] | what sits before the control inside the row |
field-row | [data-pct-part="field-row"] | the control's row, with its prefix and suffix |
field-suffix | [data-pct-part="field-suffix"] | what sits after the control inside the row |
field-warning | [data-pct-part="field-warning"] | the message when the value is allowed and suspect — after the error, before the hint, in the warning tone with a glyph and a hidden word (0087) |
field-label-aux-item | [data-pct-part="field-label-aux-item"] | one projected item inside the label slot |
field-message-aux-item | [data-pct-part="field-message-aux-item"] | one projected item inside the message slot |
field-prefix-item | [data-pct-part="field-prefix-item"] | one projected item inside the prefix |
field-suffix-item | [data-pct-part="field-suffix-item"] | one projected item inside the suffix |
Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.
| Token | Styles |
|---|---|
--pct-field-affix-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of an affix. |
--pct-field-bg{pct.surface}#ffffff · #0f172a | Background of the field. |
--pct-field-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293b | Background of the field while disabled. |
--pct-field-border{pct.border-strong}#64748b | Edge colour of the field. |
--pct-field-border-disabled{pct.border}#e2e8f0 · #334155 | Edge colour of the field while disabled. |
--pct-field-border-focus{pct.primary}#2563eb · #60a5fa | Edge colour of the field when focused. |
--pct-field-border-hover{pct.text-muted}#334155 · #e2e8f0 | Edge colour of the field on hover. |
--pct-field-border-invalid{pct.danger}#b91c1c · #f87171 | Edge colour of the field when invalid. |
--pct-field-border-warning{pct.warning}#b45309 · #fbbf24 | Edge colour of the field under a warning (0087). |
--pct-field-fg{pct.text}#0f172a · #ffffff | Text colour of the field. |
--pct-field-fg-disabled{pct.text-disabled}#64748b | Text colour of the field while disabled. |
--pct-field-fg-invalid{pct.danger}#b91c1c · #f87171 | Text colour of the field when invalid. |
--pct-field-fg-warning{pct.warning}#b45309 · #fbbf24 | Text colour of the warning line (0087). |
--pct-field-font-size{pct.font.size.md}14px | Font size of the field. |
--pct-field-font-size-lg{pct.font.size.lg}16px | Font size of the field at size lg. |
--pct-field-font-size-sm{pct.font.size.sm}13px | Font size of the field at size sm. |
--pct-field-gap{pct.space.3}8px | Gap between the field's label, control and hint. |
--pct-field-height{pct.control.height.md}36px | Height of the field. |
--pct-field-height-lg{pct.control.height.lg}44px | Height of the field at size lg. |
--pct-field-height-sm{pct.control.height.sm}28px | Height of the field at size sm. |
--pct-field-hint-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the hint. |
--pct-field-label-fg{pct.text}#0f172a · #ffffff | Text colour of the label. |
--pct-field-label-gap{pct.space.2}4px | Gap inside the label. |
--pct-field-padding-x{pct.space.4}12px | Horizontal padding of the field. |
--pct-field-padding-x-lg{pct.space.5}16px | Horizontal padding of the field at size lg. |
--pct-field-padding-x-sm{pct.space.3}8px | Horizontal padding of the field at size sm. |
--pct-field-placeholder-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the placeholder. |
--pct-field-radius{pct.radius.md}8px | Corner radius of the field. |
A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.
[data-theme='brand'] {
--pct-field-border-focus: #0f766e;
--pct-field-bg: #f0fdfa;
--pct-field-fg: #134e4a;
} Pattern: a native <input> — type, the browser's autofill and the mobile keyboard modes are all preserved
None of its own — fully native (req-api-platform).
Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
apps/sandbox-e2e/src/a11y.spec.ts (the /text view)apps/sandbox-e2e/src/visual.spec.tsforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.tsprefers-reduced-motionapps/sandbox-e2e/src/preferences.spec.tsapps/sandbox-e2e/src/field-hitarea.spec.ts (guaranteed by the wrapper)apps/sandbox-e2e/src/size.spec.ts — size belongs to the wrappertools/check-styles.mjs (point 5, req-token-logical) over the stylesheet, plus the text-types-rtl baseline in apps/sandbox-e2e/src/visual.spec.tsapps/sandbox-e2e/src/hydration.spec.tslibs/components/field/src/field-controls.spec.ts, apps/sandbox-e2e/src/forms.spec.ts — signal forms, [formControl] and [(ngModel)], each starting from a non-empty value; a form's reset — reset() keeping the value, reset(value) showing it, the form pristine: libs/components/field/src/field.spec.ts › signal formslibs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts) — the component exposes no parts of its own; the field entrypoint's inventory covers it in the same rowcontrast.policy.jsonlibs/tokens/src/contrast.policy.jsonPCT_TEXTStools/check-texts.mjs — no strings of its ownlibs/components/size.snapshot.md — the ./field row, the entrypoint the text sharesdocs/acr/at/ — three readers, whole sandbox, and the input type reaches the announcement: VoiceOver names it outright (E-mail john@example.com email field, Search… search text field, Password secure text field), NVDA gives the behaviour instead (E-mail, edit, has auto complete, john at example dot com, Password, edit, protected), Orca sits between (Password · password text., Search · entry · Search…). A password value is spoken by none of the three, which is the one thing here that had to be true/components/text on the published site — prerendered, the demo's own source is the code tabRead at build time from the tracked snapshots, not typed here.
DefaultValueAccessor rests on a heuristic. The control detects an NgControl without a FormField and then gives up ownership of the value. The condition has already been too wide once and rendered an empty field against a non-empty model (lesson-20, lesson-26) — it is the most fragile part of this control.textarea — autosize is a separate component, not built yet.