Skip to content

[DnD] Answer Tile Component - #4099

Draft
SonicScrewdriver wants to merge 27 commits into
dnd-action-menufrom
dnd-answer-tile
Draft

[DnD] Answer Tile Component#4099
SonicScrewdriver wants to merge 27 commits into
dnd-action-menufrom
dnd-answer-tile

Conversation

@SonicScrewdriver

@SonicScrewdriver SonicScrewdriver commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary:

tbd

Issue: LEMS-4363

Test plan:

@github-actions

Copy link
Copy Markdown
Contributor

npm Snapshot

Want to try this PR's changes before it merges? Comment /snapshot below and we'll publish an npm snapshot you can install right away.

@SonicScrewdriver SonicScrewdriver changed the title Make jquery 2.1.1 a direct dep of Perseus to decouple from frontend (#4096) [DnD] Answer Tile Component Aug 19, 2026
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 519 kB

ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.6 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 6.31 kB
packages/math-input/dist/es/index.js 98.6 kB
packages/math-input/dist/es/strings.js 1.63 kB
packages/perseus-core/dist/es/index.item-splitting.js 13.1 kB
packages/perseus-core/dist/es/index.js 28.3 kB
packages/perseus-editor/dist/es/index.js 106 kB
packages/perseus-linter/dist/es/index.js 10.1 kB
packages/perseus-score/dist/es/index.js 9.86 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/index.js 203 kB
packages/perseus/dist/es/strings.js 12.9 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.12 kB

compressed-size-action

SonicScrewdriver and others added 10 commits August 20, 2026 23:21
AnswerTile is the card a learner moves into a blank. It renders authored
markdown content (text, TeX, image, or empty) via the Perseus Renderer,
composes DndActionMenu at its leading edge, and exposes parent-set scored
states (rest / correct / incorrect / disabled).

Notable decisions:
- The menu is fed by a data prop rather than a ReactNode slot, so the
  label and disabled-ness flow down once from the tile instead of being
  wired twice by every widget. The config type is derived from
  DndActionMenuProps so the two components can't drift.
- The renderer's global .paragraph styles (Lato 18px, 22px vertical
  margins) are reset inside the tile — without this, tiles render far
  taller than the design.
- Scored states swap the menu for a decorative check/x glyph, so
  correctness is never conveyed by color alone; announcing it is the
  widget's job.
- Scored borders are 2px vs the 1px rest border; scored states give up
  the extra width from padding so the tile doesn't shift size on state
  flips.
- The on-hover-or-focus menu visibility (inline blanks) is CSS-only, so
  the opener stays focusable and reachable for keyboard and screen
  reader users.

The placeholder tiles in the Action Menu and Choice Bank stories are
replaced with the real component. Tile-level hover/drag styling is
deferred to the dnd-kit wiring ticket.
Rewrite the AnswerTile comments in short, active sentences so they are
easier to read.
- Treat whitespace-only content as empty so the tile always has a
  visible minimum width and a spoken label.
- Merge the check/x icon tests into one honestly-titled test: jsdom
  cannot tell the two glyphs apart because the graphic lives in a
  generated stylesheet.
- Correct the content-width comment: TeX does not wrap.
- Use the shared menu testdata generator in the ChoiceBank stories and
  drop the tile story that duplicated them.
- Explain the always-false disabled prop at the menu render site.
leadingBox told readers where the element sits, not what it is. It is
now menuOrScoredIcon, which states the one-or-the-other rule directly.
stateIcon is now scoredIcon, since only scored states have icons.
The single menu-or-icon variable needed an umbrella name that never
read well. Two small named components state what renders and when, and
the render conditions in AnswerTile show they cannot appear together.
The menu opener carried its own 6px trailing margin, so the tile needed
two different wrappers: a bare one for the menu and a sized one for the
scored icon. The margin now lives on the tile's single .startContainer,
which holds either element and owns the gap before the content.
The stateClasses record added indirection that no other CSS-module
component uses. The class names match the state names, so styles[state]
is enough.
The single state prop bundled two independent facts and needed a novel
"rest" value for the default. Scoring makes the two facts exclusive by
position: a placed tile shows a correct or incorrect result, and an
unused bank tile is disabled. The tile now models them the way radio
does: an optional showCorrectness ("correct" | "incorrect") and an
optional disabled boolean, with the default look on the base class.
A one-line tile computed to 42px: 22px line height, 16px padding, and
2px of border. The design sets 48px for one-line tiles, so the tile now
has a 48px minimum height. Taller content still grows the tile.
SonicScrewdriver and others added 14 commits August 21, 2026 16:16
The AnswerTileMenuConfig doc described the type derivation without ever
naming the data. It now leads with the contents (move targets,
callbacks, remaining-use count). Also fixes the menu prop doc, which
still referred to the removed state enum.
The keyof Pick guard protected against a silent stale key after a menu
prop rename, but the tile also hard-codes label and disabled at the
render site, so a rename already fails compilation there. Plain Omit
reads better and loses no real safety.
The nested menu config object and its derived type existed only because
label and disabled belong to both the tile and the menu. The menu's
widget-facing fields are now plain tile props (moveTargets, onMove,
clearFromLabel, onClear, remainingUses, menuRef), so FITB passes label
once and every field is a plain type traceable from widget to menu.

moveTargets and onMove are now required: every live tile can be moved,
so there is no separate no-menu mode. The menu hides only on scored and
disabled tiles. An empty moveTargets array stays valid — a placed tile
in a one-blank exercise only offers Clear (new story covers this).

Adds a generateAnswerTileProps testdata generator and exports
AnswerTileProps for it and for widget call sites.
The hidden-until-hover menu behavior for inline blanks is under
discussion with design: the Figma frames remove the handle's space,
which forces a layout shift on reveal, while the built version kept the
space and hid the handle visually, which matched neither the frames nor
the readability goal. The tile now always shows the menu on unscored,
enabled tiles. The FITB ticket can add the agreed behavior back once
the design question resolves.
…led precedence

- Inline TileActionsMenu and TileScoredIcon: both were a single span
  with the same startContainer styles around one child, so the
  indirection added nothing.
- Fix the disabled class so showCorrectness wins when both props
  arrive, matching the documented precedence.
- The content wrapper is a div: the renderer emits block paragraph
  divs, which are not valid inside a span.
- Remove the data-testid attributes; tests now query by role and pass
  every asserted value explicitly instead of relying on generator
  defaults. Add an onClear passthrough test covering the Clear-only
  (empty moveTargets) path through the real menu.
- Trim the label prop doc: the empty-tile value is the widget's
  concern.
Keep only tests that catch failures nothing else would: the Renderer
wiring (text and image content), the empty-tile spoken label, the
menu-suppression logic for scored and disabled tiles, and the optional
props (onClear, menuRef) whose forwarding could be dropped without a
compile error. The removed tests covered behavior the menu's own suite
already pins (onMove) or asserted raw TeX source text.

Comments on the optional-prop tests record why each exists, so future
cleanups have the rationale at hand.
Matches the svg-image tests. The mocked loader never fetches the URL,
and a localhost address makes that clearer than a third-party domain.
The menu and the scored icon shared the same startContainer styles in
two sibling spans. One span now holds whichever shows. The explicit
aria-hidden is gone: an unlabeled PhosphorIcon hides itself from
assistive technology by default.
The tile has three valid states, which need exactly two checks: does
the tile get a start slot (not when disabled), and what goes in it
(the menu, or the icon when scored). The extra condition existed only
to give the invalid scored-and-disabled combination a defined winner.
The props doc now forbids that combination instead of the code
handling it.
The 64px minimum previously applied only to empty tiles. The Figma
frames show it holds for all tiles: the empty tile (64px) and the
single-digit tiles (65px) are the narrowest in the design, and the
smallest image preset (24px square) yields an 80px tile. A universal
minimum on the tile removes the empty-only class, so the empty check
has one job left: the screen-reader label. The content area now grows
to keep narrow content centered inside a minimum-width tile.
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