Skip to content

feat(combobox-web): headless combobox, migration steps 1 to 4 - #2504

Open
r0b1n wants to merge 31 commits into
mainfrom
feat/combobox-mobx-store
Open

r0b1n wants to merge 31 commits into
mainfrom
feat/combobox-mobx-store

Conversation

@r0b1n

@r0b1n r0b1n commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Research branch for the headless combobox; see docs/design/headless-widgets.md and docs/design/headless-combobox.md, both added here. Built on the unit-test improvements of #2499, merged into main on 2026-11-03; this branch is rebased on that main and targets it.

Migration steps 1 to 5 and 7 of the combobox design doc, as commits (step 7 taken before 6: a second View tests the contract, the package move does not):

  1. docs: add headless widgets design docs — the two design documents.
  2. build(combobox-web): add mobx, mobx-kit and brandi, run jest in strict action mode — dependencies, the jest.setup.ts of test(combobox-web): characterisation tests at the selector seam and typed test mocks #2499 gains enforceActions: "always" and [MobX] warnings turned into errors.
  3. refactor(combobox-web): make the selector store observable with mobx — every selector and provider annotated with makeObservable; the setState({}) trigger kept so this commit is a pure store change.
  4. refactor(combobox-web): replace the forced re-render with a snapshot hook — useObservableSnapshot (useSyncExternalStore over a MobX reaction) subscribed to the search term.
  5. docs(combobox-web): record migration step 1 as an openspec change — openspec/changes/combobox-mobx-store.
  6. feat(combobox-web): add the combobox core, its container and view model — src/core/: the generic envelope, the combobox ViewModel contract, and the composition root in the style of datagrid-web and gallery-web: comboboxConfig (static properties read once, including the selector family), ComboboxGateProps (the Mendix values the Core derives from), CB_TOKENS, ComboboxContainer + createComboboxContainer, FilterInputEventService (the debounced filter-input action as a setup component, disposed on unmount), ComboboxCore (injected with selector, config, gate and the service; cached slices, bound commands) and useComboboxCore. SingleSelection and MultiSelection read the snapshot only. getSelector, useGetSelector and the hand-built preview core props are gone; the editor preview uses createPreviewContainer.
  7. refactor(combobox-web): drop the deprecated search-term callback — onAfterSearchTermChange and its three tests.
  8. docs(combobox-web): record migration step 2 as an openspec change — openspec/changes/combobox-viewmodel, status lines and the composition-root decision in the design docs.
  9. feat(combobox-web): own the open state, input text and highlight in the core — isOpen, highlightedIndex, inputValue (the search term) and highlighted per option on the ViewModel; open/close/toggle/highlight/enter/leave commands; enter and leave actions through the gate; the lazy first page requested by open() once per datasource filter, through a setup reaction.
  10. refactor(combobox-web): extract the behavior and popover hooks from the views — useComboboxBehavior runs downshift fully controlled against the ViewModel and returns the section 4 prop getters; useComboboxPopover wraps the floating menu; the views and menus take the ViewModel only; the two downshift hooks, useActionEvents and useLazyLoading are gone. Seven Behavior tests cover the "must survive" items the component specs did not. Single mode calls getDropdownProps of the always-mounted useMultipleSelection, which the strict Jest setup of test(combobox-web): characterisation tests at the selector seam and typed test mocks #2499 requires.
  11. docs(combobox-web): record migration step 3 as an openspec change — openspec/changes/combobox-behavior, status line in the design doc.
  12. refactor(combobox-web): derive the selectors from the props gate — the selectors and their providers are computeds over the props gate and the frozen config, the way the data grid and gallery models derive from theirs; the datasource limit, filter, selected-value reload and selection sync are setup reactions; container.setProps is the gate alone; the association and database families share one DatasourceOptionsProvider and one DatasourceCaptionsProvider; the preview selectors are plain objects. The selector specs resolve their selector from the container with the setup host running. Five deliberate behaviour changes are listed in the change's design.md; the user-visible one (a static combo box follows an external clear of its attribute) has a CHANGELOG line. Also fixes a latent Core bug: the commands were bound before makeObservable, so the first Core instance in a process had unwrapped actions and pnpm run test -t "sets option to selected item" failed in isolation. 59 files, +1466 / -2295 in src/.
  13. docs(combobox-web): record the selector gate as an openspec change — openspec/changes/combobox-selector-gate, status and decision lines in the design docs, the changelog line.
  14. docs(combobox-web): record the green e2e run for the selector gate — tasks and design notes of the selector gate change.
  15. refactor(combobox-web): classes map, data attributes and caption markup in the view — src/components/classes.ts holds every class name of the Atlas View with the released names as values; state is a data attribute on the element (data-open, data-highlighted, data-selected, ...) instead of a modifier class, carried by the Behavior getters where downshift owns the state and set by the View otherwise; Combobox.scss targets the attributes. ComboboxOptionVM.content is the configured custom content only and the new OptionContent component places it per hints.customContent; the captions providers lose render and their React imports; selectedContent is selectedCaption. The chip remove control gets its own class and aria-selected is always present. This is the one step that changes the DOM on purpose: the five snapshots were updated once and the diff against step 3 is additive, two data attributes next to the classes that were there. The modifier classes stay for one release and are deprecated in the CHANGELOG, so no app CSS breaks.
  16. docs(combobox-web): record the classes map as an openspec change — openspec/changes/combobox-classes, the implemented classes and attributes table in the design doc.
  17. docs(combobox-web): keep the openspec changes free of personal names — two wording fixes.
  18. docs(combobox-web): record the green e2e run for the classes map — tasks and design notes of the classes change.
  19. refactor(combobox-web): split the atlas view into parts with classes and parts props — ComboboxView is one component for both modes, assembled from eleven parts (InputContainer, Placeholder, Chip, Clear, Toggle, Validation, Menu, SelectAll, Option, NoOptions, Loader), each something a View author would replace as a whole; classes replaces the Atlas class of the given keys, parts the given parts, and the parts read the resolved map through useComboboxClasses. The two mode views, their menus and the three wrappers are gone. The DOM is unchanged: the five snapshots pass as they are.
  20. docs(combobox-web): record the parts split as an openspec change — openspec/changes/combobox-parts, sections 7 and 8 of the design doc, the decision row in the generic doc.
  21. docs(combobox-web): record the green e2e run for the parts split — tasks and design notes of the parts change.
  22. feat(combobox-web): compose the a11y status message in the core — a11y.statusMessage is derived from the selection, the open flag and the option count through t; the Behavior hands it to downshift. Three a11y-only wording differences, listed in the change's design.md.
  23. fix(combobox-web): mark the multi-select listbox as multiselectable — aria-multiselectable on the menu props in multi mode, found by the conformance suite; the three multi-select snapshots gain that line.
  24. test(combobox-web): add the view conformance suite and run the atlas view through it — src/conformance/describeComboboxConformance.tsx, sixteen role-based cases that take any component rendering the ViewModel, mounted over a live Core; ComboboxView.conformance.spec.tsx runs them for the Atlas View. Section 11 of the generic design.
  25. feat(combobox-web): add a bare sandbox view and run it through the conformance suite — src/sandbox/BareView.tsx, the throwaway second View: plain markup with its own keyboard handling and ARIA over the ViewModel commands, no Behavior, no Popover, no classes, not in the bundle. Passes the suite unchanged; what a View without the Behavior does itself is in the change's design.md.
  26. docs(combobox-web): record the second view and the conformance suite as an openspec change — openspec/changes/combobox-second-view, status lines, a decision row, the toggle text key as an open question.
  27. feat(combobox-web): split the view model into the widget envelope and the input facet — after hosting five Views (plain CSS, Tailwind, Material UI, Ant Design, Siemens iX) in a Studio Pro app over the same ViewModel: WidgetViewModel keeps the five fields every pluggable widget fills (id, status, hints, t, tabIndex); InputViewModel is Studio Pro's input bundle (readOnly, validation, a11y, enter, leave); the combobox composes both and keeps statusMessage in its own a11y type. placeholder is split from selectedCaption, emptyOption leaves the text map, className and style leave the envelope. Section 7 of the generic design records the survey of the 55 widget XMLs behind it; the conformance suite pins the empty option text and the id the label targets.
  28. feat(combobox-web): render a view from the app project when the widget name asks for one — the step 8 trial, ahead of step 6: a widget named *_atlas|_tailwind|_mui|_antd|_ix lazily renders the Combobox export of the matching @combobox-ui/<name> package from the app's node_modules; the packages are external to the widget bundle so the app bundler makes a chunk per package. The name suffix stands in for uiVariant, the fixed import list for the registry (open question in the design).
  29. refactor(combobox-web): move the core and the atlas view into headless packages — step 6. Two private workspace packages under packages/kits, built with tsc like the shared plugin packages: @mendix/widget-kit (the envelope and input facet, focusBoundaryProps, useObservableSnapshot) and @mendix/combobox-kit (core, selectors, hooks, the Atlas View as src/atlas, conformance, preview selectors, sandbox) behind four entries: the contract, ./atlas, ./conformance, ./preview. The widget keeps the XML, editor files, icons, stylesheet, wrapper, trial registry and its three specs, which run against the package's dist. The package carries a copy of the generated typings guarded by an equality spec. 91 files moved with history; the five snapshots pass unchanged; the MPK bundles the package.
  30. docs(combobox-web): record the package move as an openspec change — openspec/changes/combobox-package-move, step 6 status, the two-package layout and the decision row.

Deferred on purpose and listed in the changes: a11y.statusMessage stays composed in the Behavior until a View without Behavior needs it (step 7); the classes and parts props on the View are step 5. brandi-react is not added: views read a plain snapshot and never resolve from the container.

Verification at the tip: pnpm exec tsc --noEmit clean in the widget; pnpm run test 23 suites and 280 tests across the three packages (widget-kit 3, combobox-kit 226, combobox-web 51) (the 12 view cases of #2499 included), 5 snapshots updated once in the classes commit (additive diff) and once in the multiselectable fix (one attribute line in the three multi-select snapshots); lint 0 errors; pnpm run build OK. E2E ran locally against the test project for step 3: 52 passed, 2 skipped, and the 3 failures were regressions of the first cut (single-select leave kept the typed text, multi-select reopened highlighted), fixed and pinned by unit tests before the push; the re-run after an app restart was green. The local E2E run for the selector gate change was green after an app restart. The local E2E run for the classes commit was green with the visual snapshots unchanged. The local E2E run for the parts commit was green after an app restart; it changes no DOM. The second-view commits change one attribute; the local E2E run for them is pending. Rebased on main after #2499 merged on 2026-11-03 (strict downshift console check and view cases in the base), so CI runs the unit and E2E suites on this PR from now on.

@r0b1n
r0b1n force-pushed the feat/combobox-selector-safety-net branch from d7cf817 to b6e38a2 Compare October 30, 2026 08:34
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch 2 times, most recently from e3126e8 to f281abc Compare October 30, 2026 08:50
@r0b1n
r0b1n force-pushed the feat/combobox-selector-safety-net branch 2 times, most recently from e251deb to ca117d9 Compare October 30, 2026 08:58
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch from f281abc to ed6c215 Compare October 30, 2026 08:58
@r0b1n
r0b1n force-pushed the feat/combobox-selector-safety-net branch from ca117d9 to 2a2a4ee Compare October 30, 2026 13:30
@r0b1n r0b1n changed the title refactor(combobox-web): convert the selector store to MobX (migration step 1) feat(combobox-web): headless combobox, migration steps 1 and 2 (mobx store, view model) Oct 30, 2026
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch 2 times, most recently from 7095a5c to 08c22d9 Compare October 30, 2026 17:24
@r0b1n r0b1n changed the title feat(combobox-web): headless combobox, migration steps 1 and 2 (mobx store, view model) feat(combobox-web): headless combobox, migration steps 1 to 3 (mobx store, view model, behavior) Nov 2, 2026
@r0b1n r0b1n changed the title feat(combobox-web): headless combobox, migration steps 1 to 3 (mobx store, view model, behavior) feat(combobox-web): headless combobox, migration steps 1 to 3 and the selector gate Nov 2, 2026
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch from 10b1623 to 273cb7d Compare November 2, 2026 13:54
@r0b1n r0b1n changed the title feat(combobox-web): headless combobox, migration steps 1 to 3 and the selector gate feat(combobox-web): headless combobox, migration steps 1 to 4 Nov 2, 2026
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch 2 times, most recently from a5544e9 to dd76b31 Compare November 3, 2026 12:15
Base automatically changed from feat/combobox-selector-safety-net to main November 3, 2026 14:01
r0b1n added 12 commits November 3, 2026 15:04
Design docs for the headless widget concept (generic) and the combobox
contract with its safety-net rules and the 8-step migration plan.
…t action mode

Adds mobx 6.12.3, @mendix/widget-plugin-mobx-kit and brandi for the
headless combobox plan: the selector store becomes observable (step 1)
and the core is assembled from a brandi container like the data grid
and gallery widgets (step 2). The package now runs Jest through its own
jest.config.js so a setup file can enable enforceActions: always and turn
the MobX strict-mode warning into a test failure.
Every selector, options provider, captions provider and the lazy-load
provider now declares its state with makeObservable. Mendix values enter as
observable.ref only, props updates and setters are actions, derived getters
are computed. The Selector interface and the re-render trigger in
useGetSelector are unchanged so the characterisation tests pass as they are.
StaticOptionsProvider loses three field redeclarations that would have
shadowed the base class observables.
…hook

useGetSelector no longer registers onAfterSearchTermChange to call
setState. It subscribes to the observable search term through
useObservableSnapshot, a useSyncExternalStore wrapper around a MobX
reaction. The hook is the snapshot hook from the headless design docs and
will read the ViewModel in step 2.
Adds the combobox-mobx-store change (proposal, design, tasks) and a status
line under step 1 of the headless combobox migration plan.
Migration step 2 of docs/design/headless-combobox.md. The core derives
a plain ViewModel from the selector store as cached slices, so a View
can memoise on them, and the two selection views render from that
snapshot only. The core is assembled by a brandi container like the
data grid and gallery widgets: a frozen config for the static Studio
Pro properties, a props gate for the Mendix values, the selector family
bound per instance, and a setup host for the debounced filter-input
action. getSelector and useGetSelector are replaced by the container
and useComboboxCore; the editor preview uses the same container over
the preview selectors.
Migration step 2, third commit. onAfterSearchTermChange leaves the
OptionsProvider interface, the base provider and the two preview
providers. The view model subscription covers the search term, so the
side channel has no caller. Its three characterisation tests go with it;
the remaining defaults test keeps its file.
Adds openspec/changes/combobox-viewmodel with the container, config and
gate decisions, ticks task 3.2 of combobox-mobx-store, and records the
step 2 status and the composition-root decision in the design docs.
…he core

Migration step 3, first half. The core owns isOpen and highlightedIndex
and exposes the search term as inputValue; options carry a highlighted
flag; open/close/toggle/highlight, enter/leave and a reset reason for
setInputValue join the commands. The enter and leave actions arrive
through the gate, so the core runs them itself and leave closes the
menu. The lazy first page is requested by open(), once per datasource
filter and read-only state, through a setup reaction. The container
creates the core upfront so that reaction is registered before the
setup host runs.
…he views

Migration step 3, second half. useComboboxBehavior runs downshift in
fully controlled mode against the ViewModel and returns prop getters
for the root, input, toggle, menu, options, chips, clear and select-all
parts; useComboboxPopover wraps the floating menu. The two downshift
hooks, useActionEvents and useLazyLoading are gone, the selection views
and menus take the ViewModel only, and the wrapper components take plain
prop objects instead of downshift getters. The enter and leave callbacks
leave the Selector interface. Every item of the "behaviour that must
survive" list is covered by a component spec; the DOM is unchanged.
Adds openspec/changes/combobox-behavior with the controlled-downshift,
input-value, enter/leave and lazy-first-page decisions, ticks task 3.2
of combobox-viewmodel, and records the step 3 status in the design doc.
The selector seam rewrite is split off as the next change.
The selectors and their providers become computed views over the props gate and the frozen
config instead of copying props in updateProps. The datasource limit, filter, selection sync and
keep-selection writes move into setup reactions. The container pushes props through the gate only.
Selector specs resolve their selector from the container with the setup host running.

Also binds the Core commands after makeObservable: the first Core instance in a process had
unwrapped actions, which failed the component specs when run in isolation.
r0b1n added 14 commits November 3, 2026 15:04
Proposal, design and tasks for combobox-selector-gate; the step 3 change ticks its follow-up; the
design docs record the status and the decision; the changelog gets the static-selection fix.
…up in the view

The Atlas View reads its class names from one map with the released names as values, and
state is a data attribute on the element next to the modifier class it replaces. The Behavior
getters carry the state of their element. Option content is the configured custom content
only; the View places it per the custom-content hint. The chip remove control gets its own
class, and aria-selected is always present. The five DOM snapshots are updated on purpose;
the diff against step 3 is additive.

The modifier classes are deprecated and stay for one release, so app CSS keeps matching.
Adds openspec/changes/combobox-classes, the step 4 status and the implemented classes and
data attributes table in the combobox design doc, a decision row in the generic doc, and
ticks the step 4 item of combobox-behavior.
Local E2E against the test project after the app restart was green with the visual
snapshots unchanged. Ticks task 4.5 of the classes change; CI E2E (4.6) still waits
for the rebase on main.
…and parts props

ComboboxView is one component for both modes, assembled from eleven parts, each something a
View author would replace as a whole. Its classes prop replaces the Atlas class of the given
keys and its parts prop the given parts; the parts and the components inside them read the
resolved class map through a context hook. The two mode views, their menus and the three
wrappers are gone. The DOM is unchanged: the five snapshots pass as they are.
openspec/changes/combobox-parts, the parts and view contract sections of the design doc, the
decision row in the generic doc.
Local E2E against the test project after the app restart was green with the visual snapshots
unchanged. Ticks task 4.5 of the parts change; CI E2E (4.6) still waits for the rebase on main.
The live-region text was assembled inside the Behavior's downshift callback, so a View without
the Behavior had nothing to announce. The Core now derives `a11y.statusMessage` from the
selection, the open flag and the option count, and the Behavior hands that string to downshift.

Three a11y-only differences from the former callback, all deliberate: single mode no longer
announces the hardcoded "No options selected." while nothing is selected; multi mode announces
the option count regardless of the highlight; a closed multi-select with no options announces
its selection instead of the no-options text.
The menu of a multi-select rendered without `aria-multiselectable`, which the ARIA listbox
pattern requires for a list with more than one selectable option, and which the Behavior contract
in the design doc lists among the menu props. Found by the View conformance suite. The three
multi-select snapshots gain the attribute, nothing else.
…view through it

`src/conformance/describeComboboxConformance.tsx` takes any component that renders the
ViewModel and asserts the behaviour the combobox promises, through roles and ARIA only: a
combobox input that opens on click and with the arrow keys, a listbox whose options track
selection and highlight, selection with Enter and click, filtering and the no-options text,
dismissal with Escape and on leaving the widget together with the enter and leave actions, the
clear and remove controls, read-only, validation, select-all, multiselectable listbox and the live
region. Sixteen cases, mounted over a live Core from the props fixtures.

The Atlas View runs it in `ComboboxView.conformance.spec.tsx`. Section 11 of the generic design
doc; this is the suite a View author gets.
…nformance suite

`src/sandbox/BareView.tsx` is the throwaway second View of migration step 7: plain markup with
its own keyboard handling and ARIA over the ViewModel commands, no Behavior, no Popover, no
classes. It stands in for a View built on a component library that owns keyboard and ARIA itself,
and it passes the conformance suite unchanged. Nothing in `src/sandbox/` is imported by the
widget, so the bundle is unaffected.

What the exercise showed about the ViewModel: `select` does not close a single-select menu and
`toggleItem` does not reset the filter text, so a View without the Behavior calls `close` and
`setInputValue("", "reset")` itself; the status message had to move into the Core; there is no
text key for the toggle control, so the sandbox hides its toggle from assistive technology.
…as an openspec change

`openspec/changes/combobox-second-view`: the suite's cases, who runs it, the decisions (step 7
before step 6, a hand-rolled View instead of a component library, the status message in the
Core, the multiselectable fix) and what the second View showed about the ViewModel. Status lines
in both design docs, a decision row and the toggle text key as an open question.
@r0b1n
r0b1n force-pushed the feat/combobox-mobx-store branch from c0928be to a867629 Compare November 3, 2026 14:12
r0b1n added 4 commits November 5, 2026 15:40
… the input facet

The envelope keeps the five fields every pluggable widget fills: id, status, hints, t and
tabIndex. Input widgets add InputViewModel, Studio Pro's own input bundle: readOnly, validation,
a11y with label, required and describedBy, and the enter and leave commands. The combobox
composes both and keeps its live-region statusMessage in its own a11y type.

The combobox view model gains placeholder next to selectedCaption, which now holds the
selection's caption only; emptyOption leaves the text map because the core resolves it, including
the database display-value rule. className and style leave the envelope: a widget with the Label
system property never receives them.

Hosting five views in a Studio Pro app over the same view model drove the changes; the design
documents record the survey of the widget XMLs, the view contract on the snapshot, the focus
boundary as a helper instead of a host element and the decisions. The conformance suite pins
the empty option text and the id the label targets.
…t name asks for one

Trial of the in-project route ahead of the package move (design step 8). A widget whose Studio
Pro name ends in _atlas, _tailwind, _mui, _antd or _ix renders the Combobox export of the
matching @combobox-ui package from the app's node_modules, loaded lazily. The packages are
external to the widget bundle so that the app bundler resolves them into chunks of their own.
Any other name renders the Atlas view as before. The name suffix stands in for a uiVariant
property; the fixed import list stands in for the registry, see the open questions in the design.
…kages

Two private workspace packages under packages/kits, built with tsc like the shared plugin
packages. @mendix/widget-kit holds what every headless widget shares: the view model envelope
and the input facet, focusBoundaryProps and useObservableSnapshot. @mendix/combobox-kit holds
the core with its container and selectors, the hooks, the Atlas view with its parts and classes
(src/atlas), the conformance suite, the preview selectors and the sandbox view, behind four
entries: the contract, ./atlas, ./conformance and ./preview.

The widget keeps the XML, the editor config and preview, the icons, the stylesheet, the wrapper,
the trial registry and its three specs, which run against the package's dist with the floating
hook mocked through the package path. The package carries a copy of the generated typings as a
.ts file, guarded by a spec that fails when it drifts from the widget's file. The Behavior's root
props and the bare view use the shared focus helper. 91 files move with their history; the five
snapshots pass unchanged.
openspec/changes/combobox-package-move, the step 6 status and package layout in the combobox
design, the two-package layout and the step 6 decision row in the generic design, the package
path in the two earlier changes that pointed at it.
@r0b1n
r0b1n marked this pull request as ready for review November 6, 2026 13:02
@r0b1n
r0b1n requested a review from a team as a code owner November 6, 2026 13:02
</div>
);
}
const variant = uiVariantFromName(props.name);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 High — widget name now changes behaviour and can break existing apps. uiVariantFromName(props.name) switches the rendered View on any widget name ending in _atlas|_tailwind|_mui|_antd|_ix. An existing app with a combo box named e.g. comboBox_ix or status_atlas will, after upgrading, lazily import("@combobox-ui/ix"). That package is external to the bundle (rollup.config.mjs) and does not exist in normal apps, so the dynamic import rejects. There is no error boundary around the Suspense, so the whole page or subtree crashes. The UnavailablePlaceholder path above does not help here.

Fix: keep this trial out of the shipped widget. Put it on a research-only build or flag, or add an error boundary that falls back to <ComboboxView vm={vm} />. Move it behind a real, opt-in widget property before release. Also, the external regexp in rollup.config.mjs plus the dynamic imports leave unresolvable bare specifiers in the shipped MPK.

@github-actions

This comment has been minimized.

Remove the name-suffix trial: the view registry with its lazy imports of
`@combobox-ui/*`, the module typings and the rollup externals. The test
project in CI has no `node_modules`, so mxbuild could not bundle the widget
and the e2e job failed before any test ran. The dynamic imports return with
the `uiVariant` property in step 8.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

AI Code Review

✅ Approved: no issues found. This is a shallow review of a very large PR (see the note below).


What was reviewed

Package Change
packages/pluggableWidgets/combobox-web The widget is now a thin wrapper over @mendix/combobox-kit. The XML, editor files, SCSS and e2e specs stay in the widget. Selectors and components moved out, and specs and snapshots were updated. CHANGELOG.md has Unreleased entries.
packages/kits/combobox-kit (new) Headless core: MobX selectors, view model, behavior hooks, Atlas view parts, conformance suite and preview selectors, with unit tests.
packages/kits/widget-kit (new) Widget envelope view model, useObservableSnapshot and focusBoundaryProps.
docs/design, openspec/changes Design docs and per-package openspec changes. These are correctly placed under the package, not at the monorepo root.

Skipped (out of scope): pnpm-lock.yaml, snapshots, openspec and design prose.

CI: pending: Run code quality check, Plan E2E matrix, sigridci · unrelated failure: security/snyk ("20 tests have failed"; the Snyk code and license checks pass, so this is likely dependency-scan noise to triage separately)


Questions

  • The PR is large (about 8k additions and 5k deletions), and I did not trace every changed line. I checked the repo conventions, the changelog, the workspace wiring and the widget entry point (Combobox.tsx). I did not walk through the MobX selector rewrite or the view parts line by line. Do the unit and E2E runs on this PR confirm the "5 deliberate behaviour changes" listed in combobox-selector-gate/design.md? CI E2E has not finished yet.

Positives

  • The [Unreleased] changelog is user-facing. It covers the static-value clear fix and deprecates the modifier classes in favour of data attributes while keeping them, so existing app CSS keeps working.
  • The XML contract is untouched, and the DOM is additive only (data attributes next to the existing classes), so there is no breaking change for existing apps.
  • The new kit packages are covered by packages/*/* in pnpm-workspace.yaml. The widget depends on them via workspace:*. The describeComboboxConformance suite gives any future view a shared contract test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant