Repository navigation
fix(chip): apply removable on every path and use one error gate (CH1) - #103
Merged
Merged
Conversation
Add the design handoff for sd-icon, sd-icon-picker, sd-chip and sd-table under .sdcorejs/handoff/design-icon-chip-table/ (review of 22/10/2026 on 769f9ca). Add chip.behaviour.spec.ts for CH1. It drives A1-A8 through real keydown, focus and submit events instead of calling onAdd / onRemove with fake events (D4). It also covers mat-chip-grid writing the text of the rendered chips back into the control on blur, which turned up while checking "form reset to null" on the showcase. Rename the spec that claimed to test sd-label but checks mat-label. These specs fail until the fix commit that follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apply the sd-chip-calendar fixes to sd-chip (design handoff CH1, A1-A8),
with no API change:
- removable: each mat-chip-row gets its own boolean, and onRemove checks
it again. A predicate used to reach Material as a function, which is
always truthy, so Backspace / Delete removed locked chips.
- chips(): every item except null, undefined and '' is a chip, so 0
shows. Chips are tracked by value.
- Duplicates compare as strings, so [1] and a typed "1" are one chip.
- A null or undefined model clears the chips and sets formControl to [].
- isErrorShown() is one gate (invalid, and touched or the parent form
submitted) for the outline, <mat-error>, the hideInlineError icon and
aria-invalid.
- The remove button is named "Xoá {value}" (core.form.chip.remove, in
five locales) and uses the close icon.
- The wrapper is layout only. With sdViewDef, the face is a
role="button" that Enter or Space opens.
- With hideInlineError, the hidden error text has an id that
aria-describedby points at.
Also fix a bug that was already on main: mat-chip-grid, the value
accessor of formControl, wrote the text of the rendered chips into the
control after every blur. Numbers became strings and a form reset made
while the input had focus was undone. The grid now has a gridControl of
its own, and sd-chip marks the field touched when focus leaves it. This
also removes Material's aria-invalid host binding from the error gate.
Roll out with npm run sync. Rename the @let of sd-chip-calendar to
_removable, as in sd-chip. Add a "Chip khoá" showcase section and a
submit / reset / hideInlineError demo, with regenerated example sources.
Update CHANGELOG [Unreleased] and sd-chip.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the Node 24.22.1 pin and the single AGENTS.md guide (#102). SYNC-STATUS.md conflicted only on the Updated At timestamp of both syncs; resolved by running npm run sync again on the merged tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sdcorejs
pushed a commit
that referenced
this pull request
Oct 24, 2026
#103 (CH1) was squash-merged into main as 505d048, whose tree is the same as the CH1 branch tip c238754 this branch was built on (tree da5b7c3). Record main as a parent so the PR diff shows only CH2. The ours strategy keeps this branch's tree, which already holds everything on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 26, 2026
sdcorejs
added a commit
that referenced
this pull request
Oct 26, 2026
…sd-page-list (E1) (#106) * feat(badge): export SdBadgeData and use it for sd-table useBadge (PR 1) First step of the page-layouts handoff (doc 02). SdBadgeData describes one badge passed through an option instead of an <sd-badge> tag; sd-page [badges] will reuse it in the next step. - components/badge: new SdBadgeData { title, type, color, icon, label, tooltip }, exported from @sdcorejs/angular/components/badge. title keeps null so objects of the old internal Badge shape still type-check. - components/table: UseBadgeFunc / UseBadgeValuesFunc return SdBadgeData; SdTableDisplay.badge gains label and tooltip; the cell binds label and uses the badge tooltip only when the column has no tooltip. - Docs: SdBadgeData model section in sd-badge.md, badge shape in sd-table.md, CHANGELOG entry. - Rolled out to v20/v21/v22 with npm run sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(page): sd-page P0 — backLink, badges, width and new slots (PR 2) Second step of the page-layouts handoff (doc 01). sd-page - backLink (URL or router commands) renders a real <a routerLink> before the title; the title, description and badges still come from inputs. headerLeft still replaces the left area and then drops backLink with a dev-mode warning. - [badges]: SdBadgeData[] next to the title as md badges, round by default; badges without a title are skipped; more than two or a repeated type warn in dev mode. - width: full (default) | wide 1200 | medium 960 | narrow 720. The column limit includes 24px side padding; subheader and footer use the same padding. The host gets sd-page--wide/medium/narrow. - Slots: [sdSubheader] (in the header block, outside the scroll area), [sdAside] (320px sticky column, under the body below 1200px of viewport), [sdFooterLeft]/[sdFooterRight] (footer pinned to the bottom, hidden while empty), and [sdHeaderLeft]/[sdHeaderRight] aliases. - The title renders in <h1 class="T20M"> with its margin reset; the component is OnPush. - The new slots are projected without wrappers, so a page that uses none of them keeps its header and body DOM; only a hidden empty <footer> is added. Also - i18n: core.common.back and core.module.layout.page.actions in the five catalogs. - Theme: component tokens page-divider, page-back-fg, page-back-hover-bg. - Docs: new sd-page.md, sd-layout.md row, CHANGELOG (Added and Changed). - Showcase: components/page demo (4 sections) and registry entry. The registry spec demo total goes from 450 to 457: 4 new sections plus 3 chip sections added by #103-#105 without updating the count (showcase Karma does not run in CI). Route-shell test counts updated. - Rolled out to v20/v21/v22 with npm run sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(page): sd-page-detail and sd-page-form, toc and leave guard (PR 3) Third step of the page-layouts handoff (docs 04 and 05). sd-page - sd-page-detail and sd-page-form are selectors of SdPageComponent; the host tag picks the default width (sd-page full, sd-page-detail wide, sd-page-form medium). An explicit width still wins. - aside: 'end' (default) | 'start'. start puts the aside column first from 1200px of viewport; below that it stays under the body. - toc lists the titled top-level sd-sections of the body in the aside column as a <nav> (core.module.layout.page.toc), scrolls to a section on click and marks the section at the top of the scroll area with aria-current. The list is collected after render, because a nested section only reaches its parent's DOM when the parent view renders. Hidden below 1200px. sd-anchor is not used: it wraps its own items and owns its scroll area, so it cannot list sections of the page body. Leave guard - sdPageFormLeaveGuard (CanDeactivateFn) asks through SdUnsavedChangesService with the Core confirm dialog (SdConfirmService) and the doc 05 texts, by passing its own adapter for that prompt. SD_UNSAVED_CHANGES_CONFIRMATION_ADAPTER and sdUnsavedChangesGuard keep their behaviour. - SdUnsavedChangesConfirmOptions.adapter: per-call adapter. - registerSdUnsavedChangesForm(..., { dirty: 'value' }): dirty only while the value differs from the snapshot; default 'control' is unchanged. Also - i18n: core.module.layout.page.toc and leave.{title,message,stay,leave} in the five catalogs. - Docs: sd-page.md (selectors, aside, toc, leave guard), sd-unsaved-changes.md, sd-layout.md row, CHANGELOG. - Showcase: two more page demos (detail with the aside first, form with a table of contents); registry demo total 459. - Rolled out to v20/v21/v22 with npm run sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(page-list): sd-page-list with the E1 height chain and sd-table fill mode (PR 4) Fourth step of the page-layouts handoff (doc 03, acceptance E1 of doc 10). sd-page-list (modules/layout) - Wraps an sd-page, forwards headerRight, sdSubheader and the footer slots, and renders the router outlet for the detail child route. - Height chain: the body is a 16px padded flex column (gap 12px) and the only scroll area; the main table (the last direct sd-table, or an [sdPageListMain] wrapper, chosen again by a MutationObserver) takes the space left. - compact() = autoCompact (default true) and a viewport lower than 720px. The main table's external filter then starts collapsed; content reads compact() from SD_PAGE_LIST to fold its own summary. - detail="page" hides the list (display: none) while a child route is open, then restores every scroll position it captured and the focus of the element that opened the child. detail="drawer" keeps the list. - SD_PAGE_LIST gives the content compact, childActive and detail. sd-table - SD_TABLE_LAYOUT (new, in the table entry so the table does not depend on modules/layout) names a fill target. The table in it gets sd-table--fill: flex 1 1 0, a measured min-height (fixed parts + header + 5 rows, kept in sync by Mutation/ResizeObserver), a data area of at least header + 5 rows and a footer of at least 48px. - filter.externalFilterCollapsed sets the initial collapsed state; the user's choice holds until the default changes. Not applied when collapsible is false. - One-line rows are 36px (--mat-table-row-item-container-height), instead of Material's 52px, as agreed for every sd-table. Showcase - /demo/page-list: full-viewport live list (128 seeded orders) with ?cfg= toggles for tabs, score card, quick search, external filter, selection, record count, page size, read state, autoCompact and detail; child route :id/detail as a page or a 600px drawer. - e2e/page-list.e2e.ts: E1-1..E1-6 at 1440x960, 1440x800, 1280x768 and 1440x650 (runs in the asia-bangkok project only; layout does not depend on the zone). E1-4 measures the data area because the external filter lives inside sd-table. - components/page-list docs page (2 demos); registry and route-shell counts updated. Docs: sd-page-list.md, sd-table.md (row height, fill mode, externalFilterCollapsed), sd-layout.md, CHANGELOG (Added; Changed for the 36px rows). Rolled out to v20/v21/v22 with npm run sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(page-list): wait for page 2 rows before selecting in the E1-6 page test On the CI runner the paginator label switched to 21-40 on click while the rows were still those of page 1 until the delayed read landed, so the test checked ĐH-2400 of page 1 and found the first row of page 2 unchecked after Back. Wait for the first row of page 2 (ĐH-2380) before selecting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(page): render the sd-page header as <header>; run E1 on a bare sd-table screen Two items of the handoff left out of the first four commits: - doc 01 (a11y): the header of sd-page is a <header> element. Classes (c-page-header bg-white) and children are unchanged; the legacy DOM spec now checks the tag. sd-page.md and the CHANGELOG entry say so. - doc 10 (E1): also run E1-1/2/3 on a screen with only sd-table (no quick search, no filter row, no footer buttons) at the four viewports. v20-v22 are rolled out together with the merge of main that follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: nghiatt15_onemount <nghiatt15@onemount.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2 of 3 tasks
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.
Summary
This is CH1, the first of three sd-chip PRs in the design handoff
.sdcorejs/handoff/design-icon-chip-table/(review of 22/10/2026 on769f9ca, spec inchip.md). It fixes the behaviour and a11y issues A1–A8 and the specs D4, mostly by applying the fixes that already ship in sd-chip-calendar. No input, output or default changes, so it can ship as a patch. CH2 (input experience) and CH3 (visuals, cleanup,onSelectremoval) follow in separate PRs.The first commit adds the handoff bundle and the specs, which fail on their own. The second commit is the fix,
npm run sync, the CHANGELOG, docs and the showcase.What changed
removablepredicate reachedmat-chip-rowas a function, which is always truthy. Backspace / Delete on a chip, or Backspace twice from the empty input, removed a locked chip.isRemovable(item)), andonRemovechecks it again.@if (item)hid0but still counted it.track $indexrebuilt every chip after the removed one, so focus jumped.chips()drops onlynull,undefinedand''. Chips are tracked by value.[1, 2]plus a typed “1” became[1, 2, '1'].nullkept the chips and sent the old array.null/undefinedclears the chips and setsformControlto[].dirty || touched || submittedand the message usedtouched, so a submit without touching the field showed red with no message. Alsoaria-invalid="true"was set before any interaction.isErrorShown(): invalid, and touched or the parent form submitted. It drives the outline,<mat-error>, thehideInlineErroricon andaria-invalid.cancel) had no name, and sd-icon hid it witharia-hidden.closeicon named “Xoá {value}” (core.form.chip.remove, in vi/en/ja/ko/zh).divwas in the Tab order with no role or name, disabled included.sdViewDef, the face is arole="button"that Enter or Space opens. When disabled it leaves the Tab order and getsaria-disabled.hideInlineError, the hidden error text had no id and the input had noaria-describedby. Two duplicate@ifblocks.matSuffixelement holds the icon and the hidden text (witherrorId), and the input pointsaria-describedbyat it.Bug that was already on main (not in
chip.md)While checking “form reset to null” on the showcase, we found that
mat-chip-gridwrites back into the control on blur. As the value accessor offormControl, it waits for asetTimeoutafter every blur and then writes the text of the rendered chips into the control. The consequences:[1, 2]→['1', '2']) with nomodelChange.The grid is now bound to its own
gridControl, which nothing reads. It still needs an ngControl to compute its error state on every check. sd-chip marks the field touched when focus leaves it; moving between the input and the chips does not count. A side benefit is that Material'saria-invalidhost binding no longer fights the A5 gate. The grid'srequiredis set in code, because a[required]attribute on[formControl]would attach Angular'sRequiredValidatortogridControl.API consistency review
Following the mandatory naming and public API consistency review in AGENTS.md.
isErrorShown()and its gate,errorId,isRemovable(),removeLabel(),onViewKeydown, the view-trigger pattern, thecloseicon and the template@let _removablenow match. The@letin sd-chip-calendar was renamed from_itemRemovableto_removableto match.model), public exports and entry points.modelset tonull/undefinednow clears the chips and writes[]toformControl(A4). An untouched field with no model also holds[]instead ofnull.errorId,chips,isErrorShown(),isRemovable(),removeLabel(),onViewKeydown,onFocusOut(),gridControl,chipGrid. Each name matches sd-chip-calendar wherever it has one.core.form.chip.remove({value}) mirrorscore.form.chip-calendar.remove({date}). It is a separate key because the sd-chip-calendar strings name a day, which this one should not. Apps that supply a full customI18nCatalogmust add it, because the type isRecord<I18nKey, string>.SdRemovableChipPipeis no longer used by the template but stays exported until CH3, which removes it as a breaking change.npm run sync.npm run check:syncis green.Deviations from
chip.mdMatChipInputbindsaria-invalid = ngControl.invalidas a host binding. Giving the grid its own control solves it.@ifblocks. Merging them directly would breakmatSuffixprojection, so the icon and the hidden text share onematSuffixwrapper instead.chips()also drops exact duplicates from the display, because chips are tracked by value. The model is never rewritten.track. That is the expected result of tracking by value, and sd-chip-calendar does the same.Verification
All of this ran locally on exact Node 22.22.3:
npx ng test sdcorejs-angular --watch=false --browsers=ChromeHeadlessCI --code-coveragepasses 7897/7897. Statements 90.01%, Branches 79.35%, Functions 90.64%, Lines 92.44%.npm run lintnpx ng build sdcorejs-angularcheck:i18n,check:i18n-parity(1398 keys × 5 languages)npm run test:theme,npm run check:scss-hex,npm run test:showcase-examplesnpm run sync+npm run check:syncchip.behaviour.spec.ts, using real keydown, focus and submit events. Before the fix they failed (19, plus 2 for the blur write-back)./forms/chip/examples, 768px light and dark):aria-invalidtogether.hideInlineErrorfield pointsaria-describedbyat the hidden text.nulland untouched.Not verified:
v20 and v21 run in CI.
Showcase
removablepredicate.<form [formGroup]>with Gửi / Kiểm tra / Đặt lại buttons and a secondhideInlineErrorfield.demoSectionCountgoes from 5 to 6, and the example sources were regenerated.🤖 Generated with Claude Code