Repository navigation
web: the app's own dialogs and selects, never the browser's - #155
Merged
Merged
Conversation
Add a Vitest guard that reads every .ts source under web/apps (tests excluded) and fails on any browser dialog call (confirm, alert, prompt, with or without window., a file's own method excepted) or any native select element, naming each file and line. Fails on today's code: 17 browser dialog calls in 8 desk pages and 64 native selects in 35 files. Signed-off-by: colton <colton@futurebuild.ai>
Add gable-app-dialog and the confirmDialog/promptDialog helpers under components/ui, so pages ask with an awaited promise instead of the browser's blocking dialogs. The dialog is an alertdialog (or dialog for a prompt) with aria-modal, labelled by its title and described by its body; focus lands on the safe choice, Tab stays inside, Escape cancels, Enter confirms away from Cancel, focus returns to the opener, and a required prompt keeps Confirm off until text is typed. A danger tone draws the confirm button in safety red. One dialog is open at a time. Signed-off-by: colton <colton@futurebuild.ai>
Add gable-select under components/ui: a button showing the chosen option's label (or a placeholder in words) and the app's own popover listbox in place of the browser's native list. aria-haspopup listbox with aria-expanded, role listbox and option with aria-selected and aria-activedescendant; Up, Down, Home and End move the active option, typeahead matches on the first letters, Enter or Space chooses, Escape closes without choosing, Tab closes and moves on. Disabled options are listed but cannot be chosen, and an option may carry a group heading. The list opens upward when there is more room above. A choice fires a bubbling change event with the value on the element, so a handler written for a native select keeps its meaning. Signed-off-by: colton <colton@futurebuild.ai>
Every confirm( and prompt( call in the desk now asks through confirmDialog/promptDialog: the order confirm and cancel reason, the counter's leave-the-open-sale question (yard change, register change, till close), clearing every line of a sale, the void reason, Tech Admin's key and partner removals, the contact delete, the vehicle and driver removals, the market index value, and the saved report and schedule deletes. Removals and voids ask with the danger tone; the cancel and void reasons are required prompts. The register picker's decline path is now async and puts the picker's value back. The guard's root is corrected to web/apps so the generated api-client schema is out of its scope. Signed-off-by: colton <colton@futurebuild.ai>
The activity, product, transfer, adjustment, payment and route modals and the escalator toggle offer their choices through the app's own select: same options, values, defaults and change handling, the empty first option kept where it was a real choice. The required markers go: the submit handlers already guard on the chosen values. Signed-off-by: colton <colton@futurebuild.ai>
…lect The quote builder's unit and truck pickers, accounts payable, the bank reconciliation, the chart of accounts, journal entries, the account pages and forms, the pricing matrix, Tech Admin's AI provider and model, and the credit memo pages offer their choices through the app's own select. Aria labels carry over as the component's label; width constraints move to a wrapper div; every handler keeps its meaning. Signed-off-by: colton <colton@futurebuild.ai>
…elects become gable-select The driver route list's profile picker, the product tabs, the invoice list filters, the fleet vehicle and driver forms, the portal catalogue filters and team roles, the counter's job picker, refund method and register picker, the purchase order line products, the receive bin picker, the exposure filter, the report builder and the schedule form offer their choices through the app's own select. The receive bin picker keeps its disabled state when a line is fully received; the report builder's add-column and add-grouping pickers keep their disabled lead option and reset after a choice. Signed-off-by: colton <colton@futurebuild.ai>
The legs that stubbed window.confirm or set a native select's value now go through the app's components with three small helpers in the desk's test utilities: chooseOption (a pick from a gable-select's list), openAppDialog and answerAppDialog (the question over the page and its answer), plus typeIntoAppDialog for prompts. Every leg keeps its meaning: the counter still asks before leaving an open sale and a declined answer still keeps the till, the register picker still remembers per yard, the filters still send their contract names. Signed-off-by: colton <colton@futurebuild.ai>
Signed-off-by: colton <colton@futurebuild.ai>
Signed-off-by: colton <colton@futurebuild.ai>
The hosted e2e specs drove the browser's own chrome: selectOption on
native selects and page.on('dialog') auto-accepts for the confirms and
the void prompt. A small helper (e2e/app-controls.ts) picks a
gable-select option by label or value and answers the app's dialog; the
specs use it for the admin revokes and removals, the contact delete,
the order confirms, the counter's void reason, the customer form's tier
and terms, the product UOM, the driver's profile, the invoice and
credit memo filters and the quote line's unit. The quote spec now
asserts the unit control is the app's select of the 16 codes plus the
prompt. The customer form's tier and terms selects carry their labels
as aria-labels so the specs (and screen readers) find them.
Signed-off-by: colton <colton@futurebuild.ai>
Chromium answers a click on a non-interactive option inside a label element with a synthetic click at the label's first labelable control, which is the component's own button: every choice reopened the list. Clicks inside the list now cancel their default action, and a leg pins it. Found by the hosted e2e (the invoice and credit memo filters wrap their selects in labels); jsdom does not emulate label activation. Signed-off-by: colton <colton@futurebuild.ai>
…p-dialogs Signed-off-by: colton <colton@futurebuild.ai>
The tender round's two new Clear sale legs stubbed window.confirm, which the app's dialog replaced; they now answer the open dialog with answerAppDialog(true), keeping their meaning: tenders reset once the clear is confirmed. Signed-off-by: colton <colton@futurebuild.ai>
A dialog asked from inside a modal (Fleet's Delete vehicle and Remove driver sit inside z-[60] panels) renders under it today: the wrapper is z-50 in the root stacking context, so the question cannot be answered by mouse. These legs want a native dialog element, open while asked and gone after the answer; they fail on today's div-based render. Signed-off-by: colton <colton@futurebuild.ai>
The panel is a native dialog opened with showModal(): it sits above every page layer whatever the z-index, so a question asked from inside a modal (Fleet's Delete vehicle and Remove driver, whose panels are z-[60]) can be answered, and the page behind is inert while it is up. Escape's native cancel event routes through the same settle; a click on the dialog's own backdrop cancels; the dim veil stays a plain div below it. The fleet handlers take the vehicle or driver before the await, so a modal closed mid-question cannot turn the answer into an error. Signed-off-by: colton <colton@futurebuild.ai>
The counter's register picker asks the leave-the-open-sale question on change; the select's deferred focus restore runs after the dialog takes focus and steals it back, so the dialog's Tab trap, Escape and Enter never fire. These legs pick by click and by Enter and want the focus inside the dialog; they fail on today's code. Signed-off-by: colton <colton@futurebuild.ai>
The button is in the DOM while the list is open, so the focus restore on close no longer waits for a render: it happens synchronously in _close, before the change event dispatches. A handler that opens the app's dialog (the register picker's leave-the-open-sale question) finds the focus where the dialog expects it, and the dialog's Tab, Escape and Enter work. Signed-off-by: colton <colton@futurebuild.ai>
aria-activedescendant sits on the listbox, but the focus stays on the button, so assistive technology announces nothing as the arrows move. The leg wants the select-only combobox pattern: role combobox and aria-controls and aria-activedescendant on the focused button, with the list carrying the referenced id. Fails on today's code. Signed-off-by: colton <colton@futurebuild.ai>
The button carries role combobox, aria-controls naming the list's id, aria-expanded and, while the list is open, aria-activedescendant pointing at the active option, so a screen reader reads the option as the arrows move; the listbox holds the id. The e2e helper finds the control by tag now that its role is combobox. Signed-off-by: colton <colton@futurebuild.ai>
Seven selects carry an id a label[for] points at (the counter's job picker, the credit memo customer and reason, the schedule's format and frequency, the activity contact, the stackable tri-state), and a custom element is not labelable, so the combobox lost its field name and the label click. The legs want the name from the label property as aria-label, or from the label element with the chosen value, and the label click focusing the control. The label[for] leg fails today. Signed-off-by: colton <colton@futurebuild.ai>
When the host carries an id a label element points at and no label property is set, the combobox is named by that label together with the chosen value (aria-labelledby on the label and the value span), and the label's click focuses the control, as a native select's label does. The seven sites that lost their names get them back with no page changes; the register picker's screen-reader label, dropped in the sweep, is restored and it is named with the register it shows; the stackable tri-state drops its label property so its visible label names it. Signed-off-by: colton <colton@futurebuild.ai>
The list is absolute inside the select, so a modal with hidden or scrolling overflow (Create Route, Log Activity, the Fleet modals) cuts it off, and a transformed panel makes even fixed positioning moot. The legs want the popover attribute (the browser's top layer) and a fixed position read from the button's box, flipping up when the room is above. They fail on today's in-flow list. Signed-off-by: colton <colton@futurebuild.ai>
The list is a manual popover positioned fixed from the button's box, so a modal with hidden or scrolling overflow (Create Route, Log Activity, the Fleet modals) or a transformed panel no longer clips it; it flips up when the room is above the button. jsdom has no popover, so the calls are guarded and the fixed position carries the tests. K2's date field (PR 156) can take the same handful of lines; nothing shared was worth extracting yet. Signed-off-by: colton <colton@futurebuild.ai>
The native selects on Create Route (vehicle, driver) and Transfer Stock (from, to) carried required, so the browser refused an empty submit in its own words; with the app's selects the submit returns silently. The legs want aria-required on the combobox, an invalid read the page can turn on, and a message in words when the form is sent with nothing chosen, with no request sent. They fail on today's code. Signed-off-by: colton <colton@futurebuild.ai>
gable-select takes required (aria-required on the combobox) and invalid (aria-invalid with a safety red border). Create Route's vehicle and driver and Transfer Stock's source and destination carry them again; sending either form short a pick shows what is missing in words beside the buttons (role alert) and marks the empty picks, and sends nothing. The transfer's handler no longer returns silently, and both forms clear the message on the next choice. Signed-off-by: colton <colton@futurebuild.ai>
…cker The old select needed a drawn arrow; gable-select draws its own, so the overlay doubled it. (Review P3, small enough to take with this round.) Signed-off-by: colton <colton@futurebuild.ai>
A list that flips up sets only bottom today; the popover's own sheet holds top at 0, so the list lands at the top of the screen, detached from its control (the receive bin picker low on a long purchase order, the counter's lower pickers). The legs pin the right state: inset cleared, the bottom edge at the button's top when flipped, and the bottom edge cleared when opening downward. They fail on today's code. Signed-off-by: colton <colton@futurebuild.ai>
The popover's own sheet holds top at 0; setting only bottom left top in force and the flipped list landed at the top of the screen. The unused edge is now set to auto explicitly (the inset shorthand would do, but jsdom does not expand it, and the legs run there). Signed-off-by: colton <colton@futurebuild.ai>
The dialog element carries the panel's padding, so a click in that padding has the dialog itself as its target, the same target a backdrop click has: a slightly wide click near the edge of the void or cancel reason prompt throws away what was typed. The leg wants the padding on the panel and the dialog unpadded, and pins that a true backdrop click still cancels. It fails on today's code. Signed-off-by: colton <colton@futurebuild.ai>
…cancels A click whose target is the dialog element itself is a backdrop click, and the padding made the panel's edge part of that box. The padding moves to the inner panel and the dialog carries none: a click at the edge of a typed prompt keeps it open, and a click truly outside still cancels. Signed-off-by: colton <colton@futurebuild.ai>
…p-dialogs Gable light (PR 147) re-styled the pages this branch swept; the conflicts were its re-classed native selects against the app's own components. Kept both sides: light's token classes on the shared lines (labels, cards, hover shades, the till's refund label) and the app's dialog and selects on top. The dialog's veil moves to bg-backdrop/80 with the other modals. Signed-off-by: colton <colton@futurebuild.ai>
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.
What
The desk no longer uses the browser's own chrome. Every
confirm(/prompt(call and every native select element inweb/appsis replaced by the app's own components, and a guard keeps them from coming back.New components (web/apps/desk/src/components/ui/)
confirmDialog({ title, body, confirmLabel, cancelLabel, tone })resolves true or false;promptDialog({ title, label, required, confirmLabel, tone, initialValue })resolves the typed text or null. The dialog renders one at a time over the page asrole="alertdialog"(ordialogfor a prompt) witharia-modal="true", labelled by its title and described by its body. Focus lands on the safe choice (Cancel for a danger tone, the field for a prompt, Confirm otherwise), Tab stays inside, Escape cancels, Enter confirms when the focus is not on Cancel, and focus returns to the control that opened it. A required prompt keeps Confirm off until text is typed.tone: 'danger'draws the confirm button in safety red for removals and voids.<gable-select>is a button showing the chosen option's label (or a placeholder in words) with the app's own popover listbox:aria-haspopup="listbox",aria-expanded,role="listbox"/role="option"witharia-selectedandaria-activedescendant. Up/Down/Home/End move the active option, typeahead matches the first letters, Enter or Space chooses, Escape closes without choosing, Tab closes and moves on. Disabled options are listed but cannot be chosen, options may carry a group heading, and the list opens upward when there is more room above. A choice fires a bubblingchangeevent with the value on the element, so each page's handler keeps its meaning; pages read the value from their own state, as they already did (no page read a form's data for these selects).The sweep
<gable-select>with the same options, values, defaults, disabled states, test ids and change handling. The receive bin picker among them.Tests
src/test/browser-chrome.guard.test.ts(committed alone first, red on the old tree): reads every non-test.tsunderweb/appsand fails on any browser dialog call or native select, naming each file and line.window.confirmor set a native select's value now drive the new components through helpers insrc/test/dom.ts(chooseOption,openAppDialog,answerAppDialog,typeIntoAppDialog) and keep their meaning.Gates (from web/)
npm ci: oknpm run typecheck(every workspace): 0error TSnpx eslint .: cleannpm test: 95 desk files (1051 legs) plus the three other workspaces, all greennpm run build: exit 0, 0error TSbash scripts/check-shape.sh: OKWhat I could not see: no server was started and no screens were taken; the visual walk (the dialog over the order page, the bin picker in the dark app) is for the lead's own stack.
Written by Kimi K3 for the Gable lead.