The GitHub Actions job "Running Copilot Code Review" on texera.git/main has 
failed.
Run started by GitHub user Copilot (triggered by Copilot).

Head commit for run:
a46ddc6eb6588491eb72b27bc975f060c4cb1a75 / yangzhang75 <[email protected]>
fix(workflow-form): keep the keyboard out of a read-only input card, not only 
the pointer (#9016)

### What changes were proposed in this PR?

Closes #8943. Part of the Form View feature (#8011); from the #8439
refinements (item 5).

A read-only input card -- what a viewer without write access sees -- is
locked with `pointer-events: none`. That is hit testing only. The bound
formly controls are disabled through `props.disabled`, but the controls
a custom widget draws of its own are not: the HuggingFace audio and
image uploaders each render a plain file input that nothing disables. A
reader could Tab into one inside a locked card, open it with Enter and
choose a file.

For the audio input that is a real write. The widget POSTs the chosen
file to `/api/huggingface/upload-audio`, which authorises any signed-in
user and never asks who may write this workflow, so the file lands in
the server's temp store. Checked on a workflow shared read-only: two
presses of Tab reach the uploader inside the locked card, Enter opens
the file dialog, and the POST comes back 200 with the stored path. The
image uploader is client-side, so there the reader only makes the card
display a picture of their choosing -- a read-only card wires no
write-back and a reader's save is refused, so the operator keeps the
author's value and a run still runs it.

The card now also keeps the keyboard out, through a small directive on
it (`texeraReadOnlyCard`, applied as `[texeraReadOnlyCard]="!canEdit"`
beside the existing `read-only` class):

- Every focusable descendant is taken out of the tab order
(`tabindex="-1"`, its own value remembered) and marked
`aria-disabled="true"`, so there is nothing for Tab to reach and
assistive tech says why rather than going quiet.
- Every control the browser will refuse outright -- button, input,
select, textarea -- is also disabled natively, its previous state
remembered. Out of the tab order is not out of reach: `tabindex="-1"`
ends only sequential navigation and `aria-disabled` only announces, so a
reader's assistive tech could still focus a widget's file input and
activate it, which for the audio one posts the file (Copilot).
- A card that becomes editable gets its controls back, each with the
tabindex and the disabled state it had, and only those this directive
took.
- A control that leaves the card is let go of on the next pass: a widget
redraws its own controls while the card lives on, so a set that only
grew held every detached node for as long as the reader stayed on the
page (Copilot).
- Widgets render their own controls after the card is built and redraw
them as they go, so the locking is re-applied on every change inside the
card: a control added, one whose own tabindex is written back (ng-zorro
writes one when it enables a control), and one a widget enables again
(the audio uploader's `[disabled]="isUploading"`). The observer runs
outside Angular and sets only what is not already set, so one pass
settles it.

`inert` is what the step panel uses for its read-only mount, and the
issue suggested following it; this does not, deliberately. `inert` also
removes the subtree from the accessibility tree, and a reader's card
exists to be read -- the values are what a reader runs the workflow with
-- so that would trade this bug for a worse one. Disabled controls stay
in the accessibility tree and keep being announced. The directive's doc
comment says so, next to the code.

Nothing else changes: the pointer lock, the styles, the card's content
and the author's editing path are untouched, and the directive does
nothing at all while a card is editable.

### Any related issues, documentation, discussions?

Closes #8943, which re-files #8758 (closed). Part of #8439 (item 5) and
the Form View feature #8011. Raised by Copilot on #8437, #8438 and
#8440.

Three neighbouring things are deliberately left alone. The widgets' own
controls still do not consult the field's disabled state in their own
handlers -- fixing that is per-widget work (the HuggingFace uploaders,
the dataset picker, the resource value selector), and the card-level
lock covers every widget at once, including ones added later. The upload
endpoint itself authorises on being signed in and nothing else
(`@RolesAllowed("REGULAR","ADMIN")`, no workflow-access check), so it
would still accept a file posted by hand; closing the UI route does not
close that one, and it is worth its own issue. And the step panel's
`inert` mount has the same accessibility-tree problem described above;
changing it is not this issue's scope, and it is worth its own look.

### How was this PR tested?

Unit tests (vitest). The directive's own spec, on a host shaped like a
card (a bound input, a widget's Browse button, a link, a span, a control
with its own `tabindex`): every focusable control in a read-only card is
out of the tab order; each is marked `aria-disabled` while the card is
not inert and its text is still there; an editable card is left alone,
keeping a control's own `tabindex="3"`; a card that becomes editable
gets that `3` back, the untouched control gets no `tabindex`, and the
`aria-disabled` marks go; a control a widget adds after the card was
built is locked; a control whose widget writes its `tabindex` back, or
enables again, is locked again; a click on a locked control dispatches
no activation and the controls report disabled (jsdom does not model a
disabled control refusing focus, so that half is asserted through the
property the browser acts on); the disabled state is given back on
unlock; five rounds of a control being added and removed leave the
tracked set where it started, rather than holding the detached nodes;
the observer is disconnected when the card goes away. The page's
rendered spec wires it through the real template: a read-only viewer's
card, with a button appended the way a widget renders one, has it out of
the tab order and marked, and the card is not inert.

By hand, on two dev builds of the same base commit, one with this change
and one without, signed in as a second account the workflow is shared
read-only with. Before: from the view toggle, two presses of Tab land on
the uploader inside the locked card, Enter opens the file dialog, and
choosing a file POSTs it (200). After: the same two presses land on Run,
the card is skipped, and no request goes out. The mouse reaches nothing
in either build, and the stored workflow is unchanged in both.

Each guard was deletion-checked and turned a named test red (the
directive not applied; the tabindex write; the aria-disabled write; the
native disable; the editable check; the unlock; the original-tabindex
restore; the original-disabled restore; the release of what has left the
card; the childList and both attribute halves of the observer; the
disconnect). eslint, prettier and the production (AOT) build pass; every
statement, function and branch this PR adds is covered, and the full
frontend suite passes.

#### Video

**Before**



https://github.com/user-attachments/assets/cd1b58ff-6cc4-4958-ad78-dd00bb86f14e



**After**


https://github.com/user-attachments/assets/8f524126-d370-48f3-bc70-68497791cf5f




### Was this PR authored or co-authored using generative AI tooling?

Yes. Generated-by: Claude Code (Anthropic). Co-authored with Claude,
reviewed line by line by the author before submission.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Report URL: https://github.com/apache/texera/actions/runs/39334443164

With regards,
GitHub Actions via GitBox

Reply via email to