Skip to content

Feat/math keyboard redesign theme a11y - #90

Open
MaElaRo wants to merge 31 commits into
mainfrom
feat/math-keyboard-redesign-theme-a11y
Open

Feat/math keyboard redesign theme a11y#90
MaElaRo wants to merge 31 commits into
mainfrom
feat/math-keyboard-redesign-theme-a11y

Conversation

@MaElaRo

@MaElaRo MaElaRo commented Aug 10, 2026

Copy link
Copy Markdown

Description

This PR redesigns the on-screen math keyboard and makes it themeable and accessible, while
keeping all existing input behavior intact.

Theming. The keyboard's appearance is now fully configurable through a new
MathKeyboardTheme / MathKeyboardStyle API — background, key colors (grouped into tiers:
neutral, function, utility, primary), corner radius, spacing, and fonts. Wrap your subtree in a
MathKeyboardTheme, or pass a style straight to a MathField. When no theme is present, a
built-in default (dark) style is used, so existing users get a refreshed look with no code
changes. MathKeyboardStyle/MathKeyboardKeyStyle are value types with copyWith, so
overriding a single property is easy.

Accessibility. The keyboard is now usable with a physical keyboard, switch access, and a
screen reader:

  • Keyboard/switch navigation: tab moves between the keys as ordinary buttons and leaves the
    keyboard at the ends without trapping focus (WCAG 2.1.2); the arrow keys additionally move
    across the grid; escape dismisses the keyboard; and tab from the field steps onto the keys.
  • Screen reader: the math field is exposed as a text field that speaks its expression, every
    key is a labelled button, the cursor-navigation keys announce the new cursor context
    (e.g. "before 8", "numerator", "end of expression"), and the keyboard's sections
    (variables, formula, numbers, submit) are landmark regions the reader can jump between.
  • Localization: all spoken strings live in a new MathKeyboardSemantics object (English by
    default) that you can override to translate or adapt the announcements.
  • Large text: long-press a key to magnify it at large system text sizes (via
    large_content_viewer), or opt into growing the keys with the system text scale.

Landscape layout. In landscape the expression keyboard shows the functions and numbers side
by side with a dedicated full-height submit key, instead of paging.

Requirements & housekeeping. Uses SemanticsRole.region, so the package now requires
Flutter 3.35.1 / Dart 3.9. Version bumped to 0.4.0 with a matching CHANGELOG entry, and the
README documents the theming, accessibility, and large-text features.

Related issues & PRs

Checklist

  • I have made myself familiar with the CaTeX
    contributing guide.
  • I added a PR description.
  • I linked all related issues and PRs I could find (no links if there are none).
  • If this PR changes anything about the main math_keyboard or example package
    (also README etc.), I created an entry in CHANGELOG.md (## UPCOMING RELEASE if the change
    on its own is not worth an update).
  • If this PR includes a notable change in the math_keyboard package, I updated the version
    according to Dart's semantic versioning.
  • If there is new functionality in code, I added tests covering all my additions.
  • All required checks pass.

MaElaRo added 16 commits August 6, 2026 09:50
Restyle the on-screen keyboard to the simpleclub design and add a
configurable theme plus first-class accessibility, keeping the public
API backwards compatible.

Theming:
- MathKeyboardStyle + MathKeyboardTheme inherited widget with a
  Figma-default fallback, plumbed through MathField/MathFormField.
- Per-tier key tokens: digits on the secondary surface, operators and
  variables on the neutral filled-button surface, delete on
  secondary/weak, submit on primary; focus keeps the idle surface + ring.
- Function-key labels show example variables (x/y, x^2, sqrt{x}, ...)
  instead of empty placeholders, matching the design.

Accessibility:
- Localizable MathKeyboardSemantics seam for all screen-reader strings,
  including keyboard/section group labels.
- Keyboard is focus-reachable: tab hands focus from the field into the
  keys, arrow keys traverse the grid, escape returns to the field, and
  the keyboard stays open while either the field or a key holds focus.
- Keys expose single button nodes with spoken labels; sections are
  wrapped as Variables/Functions/Numbers/Submit groups inside a top-level
  "Math keyboard" group.
- Landscape focus order walks the number panel before the functions.
- Variable row shrinks keys past five so a partial key bleeds off the
  edge, and auto-scrolls the focused item into view.
- Keep the keyboard open under a screen reader so the keys can be reached.

Fixes:
- Hover now works on web (driven by MouseRegion instead of the
  highlight-mode-gated FocusableActionDetector callback).

Adds characterization and a11y tests covering behavior, focus, hover,
states, the variable bar, screen-reader survival, and semantic grouping.
The delete/back key stays on secondary/weak at idle but darkens to
secondary/base on hover and pressed (matching the design). This also
covers the page-toggle key, which shares the utility tier.
Rework the on-screen keyboard's focus model to the WAI-ARIA composite
pattern and improve screen-reader access.

Keyboard navigation:
- The keyboard is a single tab stop: arrow keys move between all keys as
  a grid, Enter/Space activate, and tab leaves the keyboard for the next
  page control (shift+tab returns to the field) so focus is never trapped
  (WCAG 2.1.2).
- Escape dismisses the keyboard and returns focus to the field without
  reopening it.

Screen reader:
- The field exposes an onTap semantics action so an accessibility
  activation (e.g. VoiceOver double-tap) opens the keyboard without a
  pointer.
- The keyboard stays open while focus is on the keys so it can be reached.
- Each section (variables, formula, numbers, submit) is a labelled
  landmark region, so a screen reader can jump straight to a section via
  landmark/rotor navigation. The keyboard wrapper stays a plain group.

Other:
- Rename the function keyboard's user-facing label to "Formula" (English
  fallback; consumers still inject their own translations).
- Center the magnified label within the large-content-viewer overlay.

Updates the focus, screen-reader, and semantics-group tests for the new
model.
The redesign adds new API, a new dependency, and changes the default
appearance, so bump the minor version and document it. Raise the SDK
floor to Flutter 3.35.1 / Dart 3.9, required by SemanticsRole.region
(used for the keyboard's section landmarks).
- Landscape submit is a single button, so wrap it only in a semantics
  boundary (to stay a sibling of the section landmarks) instead of a
  labelled region — a screen reader no longer announces "Submit" twice.
- Remove the _keyboardFocusScopeNode listener before disposing it and
  guard its callback with a mounted check, matching the other teardown.
- Rephrase the public dartdoc for MathKeyboardStyle/fallback to drop
  simpleclub- and design-token-specific wording that is meaningless to
  third-party consumers.
- Default MathKeyboardTheme.style to MathKeyboardStyle.fallback so a
  theme that only overrides semantics (for localization) no longer has
  to pass a full style.
Digits are on the function tier (secondary surface), not neutral; the
neutral tier is operators, decimal, parentheses, cursor keys, and
variables. Update the MathKeyboardKeyTier docs to match.
The inline comments on MathKeyboardStyle.fallback referenced internal
design-token names (filledButton/neutral, secondary/base, primary/…),
which are meaningless outside simpleclub. Describe each tier's keys and
its idle/hover behaviour in neutral terms instead.
The Features list didn't mention the 0.4.0 additions. Add bullets for
the configurable MathKeyboardTheme/MathKeyboardStyle, the accessibility
support (landmarks, arrow-key navigation, localizable semantics), and
the long-press large-text magnification, noting the large_content_viewer
dependency it relies on.
The keys are ordinary buttons, so tab now traverses them one by one (the expected behaviour) rather than the keyboard being a single tab stop with arrows-only movement; the arrow keys stay as a bonus for 2-D grid movement. To avoid trapping focus, tab past the last key exits to the control after the field and shift+tab past the first key returns to the field, instead of wrapping within the keys (WCAG 2.1.2). Escape still closes.
Tab entered the on-screen keys in raw geometric order, zig-zagging across
the two side-by-side landscape columns. Give each section an explicit
FocusTraversalOrder so tab visits the number pad first, then the formula
section, then submit, and switch the keyboard to an OrderedTraversalPolicy.
The boundary-exit check uses the same policy so tab still leaves the
keyboard at the ends instead of wrapping.
The section-level traversal order left keys inside a section to geometric
reading order, which zig-zagged across the unevenly sized number cells.
Assign each key an explicit FocusTraversalOrder under the single
OrderedTraversalPolicy (numbers 0.., variables 100, functions 200..,
submit 300) so tab follows the configured row-major layout end to end.
Keeping the orders global (no nested groups) means the boundary-exit
check still sees the true first and last key.
The page toggle is a mode switch, not content, so the long-press
large-content magnifier adds no value there for a screen-reader user.
Gate it on MediaQuery.accessibleNavigation: sighted users keep the
long-press magnifier on the '123' key, screen-reader users don't.
@MaElaRo
MaElaRo requested a review from edhom as a code owner August 10, 2026 09:44
@CLAassistant

CLAassistant commented Aug 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 360c2cda-fde4-46cd-979c-617591493d56

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The 0.4.0 release adds keyboard theming, localized accessibility semantics, responsive landscape layouts, focus traversal, keyboard activation, large-content previews, and readable math-field descriptions. It also updates public exports, SDK requirements, documentation, and widget coverage.

Changes

Keyboard redesign

Layer / File(s) Summary
Public contracts and configuration
math_keyboard/lib/src/foundation/*, math_keyboard/lib/src/widgets/math_keyboard_theme.dart, math_keyboard/lib/math_keyboard.dart, math_keyboard/pubspec.yaml, math_keyboard/README.md, math_keyboard/CHANGELOG.md
Adds public style and semantics APIs, theme inheritance, landscape key definitions, package exports, release documentation, and updated SDK requirements.
Responsive keyboard rendering
math_keyboard/lib/src/widgets/keyboard_button.dart, math_keyboard/lib/src/widgets/math_keyboard.dart, math_keyboard/test/widgets/math_keyboard_a11y_test.dart, math_keyboard/test/widgets/math_keyboard_test.dart
Adds styled portrait and landscape layouts, semantic keys, text scaling, focus traversal, keyboard activation, large-content previews, and rendering tests.
Field integration and focus behavior
math_keyboard/lib/src/widgets/math_field.dart, math_keyboard/lib/src/widgets/math_form_field.dart, math_keyboard/test/widgets/math_cursor_context_test.dart, math_keyboard/test/widgets/math_keyboard_focus_test.dart, math_keyboard/test/widgets/math_keyboard_screen_reader_test.dart, math_keyboard/test/widgets/math_keyboard_semantics_group_test.dart
Connects styles and semantics to fields, manages overlay focus and dismissal, exposes readable expression context, and tests focus and screen-reader behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScreenReader
  participant MathField
  participant MathKeyboard
  participant KeyboardButton
  ScreenReader->>MathField: activate editable field
  MathField->>MathKeyboard: open styled keyboard overlay
  MathKeyboard->>KeyboardButton: expose semantic key and focus action
  KeyboardButton->>MathField: submit key input
  MathField->>ScreenReader: announce readable expression and cursor context
Loading

Possibly related PRs

  • simpleclub/math_keyboard#89 — The redesign extends the theme infrastructure and modifies the same keyboard, button, and field integration code.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the math keyboard redesign, theming, and accessibility changes.
Description check ✅ Passed The description covers the redesign, related issues, implementation details, checklist, documentation, tests, and version changes.
Linked Issues check ✅ Passed The changes address issue #41 through configurable key colors and issue #32 through scalable key labels that prevent overflow.
Out of Scope Changes check ✅ Passed The code, documentation, tests, changelog, and version updates support the stated redesign, accessibility, theming, layout, and issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/math-keyboard-redesign-theme-a11y

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot left a comment

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.

Hi 👋🏽 Thank you for opening your first PR with simpleclub/math_keyboard ❤

You can expect a review from us soon ☺️
In the meantime, please check our contribution guidelines, the PR checklist, and the PR checks.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
math_keyboard/lib/src/widgets/math_keyboard.dart (1)

508-589: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Off-screen variable keys are unreachable by keyboard and switch access.

ListView.separated builds items lazily. Variables scrolled beyond the viewport and the cache extent have no element and therefore no focus node, so tab traversal and switch access skip them permanently. The Scrollable.ensureVisible callback only helps for keys that already exist. The row is explicitly sized so the sixth key bleeds off the edge, so this state is the normal case when a consumer passes more than five variables.

Build all variable keys eagerly in a horizontal scroll view. The variable count is small, so the lazy build gives no measurable benefit.

♿ Proposed fix: build all keys eagerly
-              return ListView.separated(
-                itemCount: variables.length,
-                scrollDirection: Axis.horizontal,
-                padding: EdgeInsets.only(left: leftPadding),
-                separatorBuilder: (context, index) =>
-                    SizedBox(width: style.rowSpacing),
-                itemBuilder: (context, index) {
-                  return SizedBox(
+              return SingleChildScrollView(
+                scrollDirection: Axis.horizontal,
+                padding: EdgeInsets.only(left: leftPadding),
+                child: Row(
+                  children: [
+                    for (var index = 0; index < variables.length; index++) ...[
+                      if (index > 0) SizedBox(width: style.rowSpacing),
+                      SizedBox(
                         width: keyWidth,
                         child: Builder(
                           builder: (itemContext) => KeyboardButton(
                             // ... unchanged
                           ),
                         ),
-                  );
-                },
+                      ),
+                    ],
+                  ],
+                ),
               );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_keyboard.dart` around lines 508 - 589,
Replace the lazy ListView.separated in the build method with an eager horizontal
scroll view that builds every variable key, preserving the existing spacing,
sizing, focus handling, and scrolling behavior. Ensure all entries in variables
have elements and focus nodes available for keyboard and switch-access
traversal, including variables beyond the initial viewport.
🧹 Nitpick comments (8)
math_keyboard/test/widgets/math_cursor_context_test.dart (1)

10-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dispose the controllers for consistency with the other test files.

Every other test file in this PR registers addTearDown(controller.dispose). These six tests create a MathFieldEditingController and never dispose it. Add the teardown so the tests stay correct if leak tracking is enabled later.

♻️ Example for the first test
       final controller = MathFieldEditingController()
         ..addLeaf('7')
         ..addLeaf('8');
+      addTearDown(controller.dispose);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/test/widgets/math_cursor_context_test.dart` around lines 10 -
65, Add addTearDown(controller.dispose) to each of the six tests after creating
the MathFieldEditingController, including the controllers configured with leaves
or functions. Keep the existing assertions and controller setup unchanged.
math_keyboard/test/widgets/math_keyboard_focus_test.dart (1)

91-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the backward exit path.

The tests cover forward Tab traversal and the forward exit through onExitToNext. The backward boundary is untested: PreviousFocusIntent at the first key must invoke onExitToField and return focus to the MathField instead of wrapping to the last key. Add a test that tabs into the keys once, then sends shift+Tab, and asserts that no key is focused and the field holds focus.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/test/widgets/math_keyboard_focus_test.dart` around lines 91 -
156, The existing focus traversal test only verifies forward exit; add a
backward-boundary test near the current tab traversal coverage that tabs from
the MathField into the keyboard once, then sends Shift+Tab and pumps focus
updates. Assert that the MathField regains focus and no KeyboardButton remains
focused, confirming PreviousFocusIntent uses onExitToField rather than wrapping
to the last key.
math_keyboard/test/widgets/math_keyboard_a11y_test.dart (1)

302-313: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the misleading german variable.

german holds MathKeyboardSemantics.fallback, which is the English default, and the overrides that follow are not a full German localization (variableLabel still returns 'Variable $name'). Rename it to base or english.

♻️ Proposed rename
-      const german = MathKeyboardSemantics.fallback;
-      final localized = german.copyWith(
+      const base = MathKeyboardSemantics.fallback;
+      final localized = base.copyWith(
         deleteLabel: 'Löschen',
         submitLabel: 'Bestätigen',
         variableLabel: (name) => 'Variable $name',
       );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/test/widgets/math_keyboard_a11y_test.dart` around lines 302 -
313, Rename the local variable `german` in the `MathKeyboardSemantics override`
test to `base` or `english`, since it references
`MathKeyboardSemantics.fallback` rather than a German localization; update its
subsequent `copyWith` usage accordingly.
math_keyboard/lib/src/widgets/math_field.dart (3)

451-462: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The open keyboard does not pick up later theme changes.

The OverlayEntry builder resolves style and semantics from this.context, but nothing calls markNeedsBuild on the entry when the ancestor MathKeyboardTheme changes. An open keyboard therefore keeps the style and the semantics strings that were current when it opened. This also affects a locale change that swaps the semantics strings.

Rebuild the entry when the field's dependencies change.

♻️ Proposed fix
+  `@override`
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    // The overlay resolves the theme from this context, so it must rebuild
+    // when the theme or the locale changes.
+    _overlayEntry?.markNeedsBuild();
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_field.dart` around lines 451 - 462, Update
the widget lifecycle around the overlay entry and its builder so an open
keyboard entry is marked for rebuild when the field's inherited theme or locale
dependencies change. Ensure the refreshed builder re-resolves style and
semantics from the current context, while preserving the existing overlay
behavior.

988-1010: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

readableExpression runs on every caret blink.

_FieldPreview is rebuilt each time _cursorOpacity changes, which happens twice per second while the field is focused. Each rebuild walks the whole expression tree and allocates a new string, even though the expression did not change. Cache the result and invalidate it in notifyListeners, or compute it only when the semantics value is actually requested.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_field.dart` around lines 988 - 1010, Cache
the readable expression used by readableExpression so repeated caret-blink
rebuilds do not traverse the tree or allocate a new string when the expression
is unchanged. Store the cached result alongside the semantics identity, reuse it
when the same MathKeyboardSemantics is requested, and invalidate the cache in
notifyListeners so edits and other field changes produce updated accessible
text.

359-373: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Use the aspect-scoped accessor.

MediaQuery.maybeAccessibleNavigationOf exists in Flutter 3.35.1 and avoids rebuilds for unrelated MediaQueryData changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_field.dart` around lines 359 - 373, Update
_maybeCloseKeyboard to use MediaQuery.maybeAccessibleNavigationOf(context)
instead of MediaQuery.maybeOf(context)?.accessibleNavigation, preserving the
existing false fallback and keyboard-dismissal behavior.
math_keyboard/lib/src/widgets/math_keyboard.dart (1)

900-921: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Traversal-order numbers are magic literals with an implicit capacity limit.

numberKeyOrder starts at 0, variables are pinned to 100, functionKeyOrder starts at 200, and submit is 300. The scheme breaks silently if landscapeNumberKeyboard ever holds more than 100 keys or landscapeFunctionKeyboard more than 100 keys. Extract the range bases into named constants so the intent and the limit are explicit.

Also applies to: 951-971

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_keyboard.dart` around lines 900 - 921,
Replace the traversal-order literals in the keyboard layout with named constants
for the numbers, variables, functions, and submit range bases. Initialize
numberKeyOrder and functionKeyOrder from the corresponding constants, and use
the same constants for variable and submit ordering in the related section,
making the 100-key range boundaries explicit.
math_keyboard/test/widgets/math_keyboard_screen_reader_test.dart (1)

61-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the deprecated semantics-owner access.

Use tester.semantics.performAction(find.bySemanticsLabel('Math field'), SemanticsAction.tap) and remove the getSemantics call and deprecation suppression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/test/widgets/math_keyboard_screen_reader_test.dart` around
lines 61 - 64, In the screen-reader test, replace the deprecated
tester.binding.pipelineOwner.semanticsOwner access and the intermediate node
from getSemantics with tester.semantics.performAction targeting
find.bySemanticsLabel('Math field') and SemanticsAction.tap; remove the
deprecation suppression.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@math_keyboard/lib/src/foundation/math_keyboard_semantics.dart`:
- Around line 341-370: The hashCode implementation for the semantics class must
match mapEquals’ order-independent behavior. In the hash list near
functionMappings and tokenMappings, replace the ordered hashing of
tokenMappings.entries with Object.hashAllUnordered while preserving the existing
entry key/value hashes.

In `@math_keyboard/lib/src/widgets/math_field.dart`:
- Around line 314-330: Update the _suppressReopen branch in the focus-change
handler so it restarts _cursorBlinkController before returning, while preserving
the one-shot suppression and state update behavior.
- Around line 835-846: Update the Semantics wrapper in the math field widget to
expose the current decoration.errorText on the outer semantics node while
preserving the existing ExcludeSemantics behavior for the child InputDecorator.
Add a widget accessibility test covering MathFormField validation and assert
that the validator’s error text is present in the resulting semantics node.

In `@math_keyboard/lib/src/widgets/math_keyboard.dart`:
- Around line 636-639: Guard the page selection in _buildPortrait so
controller.secondPage cannot force-unwrap a null page2. When page2 is
unavailable, including number-only keyboards with secondPage set to true, fall
back to page1 or numberKeyboard while preserving page2 selection when it exists.
- Around line 118-137: Add an assertion in the MathKeyboardStyle constructor
requiring maxTextScaleFactor to be at least 1.0, so invalid values such as those
passed through copyWith are rejected at configuration time and the existing
clamp in the keyboard layout remains safe.
- Around line 1059-1066: Update the basic key semantics flow around
BasicKeyboardButtonConfig and _BasicButton to pass the button value and resolve
plain-text labels with semantics.tokenLabel(value) instead of the displayed
label. Ensure \frac receives a token mapping or dedicated division label, while
unmapped digit semantics remain unchanged.

---

Outside diff comments:
In `@math_keyboard/lib/src/widgets/math_keyboard.dart`:
- Around line 508-589: Replace the lazy ListView.separated in the build method
with an eager horizontal scroll view that builds every variable key, preserving
the existing spacing, sizing, focus handling, and scrolling behavior. Ensure all
entries in variables have elements and focus nodes available for keyboard and
switch-access traversal, including variables beyond the initial viewport.

---

Nitpick comments:
In `@math_keyboard/lib/src/widgets/math_field.dart`:
- Around line 451-462: Update the widget lifecycle around the overlay entry and
its builder so an open keyboard entry is marked for rebuild when the field's
inherited theme or locale dependencies change. Ensure the refreshed builder
re-resolves style and semantics from the current context, while preserving the
existing overlay behavior.
- Around line 988-1010: Cache the readable expression used by readableExpression
so repeated caret-blink rebuilds do not traverse the tree or allocate a new
string when the expression is unchanged. Store the cached result alongside the
semantics identity, reuse it when the same MathKeyboardSemantics is requested,
and invalidate the cache in notifyListeners so edits and other field changes
produce updated accessible text.
- Around line 359-373: Update _maybeCloseKeyboard to use
MediaQuery.maybeAccessibleNavigationOf(context) instead of
MediaQuery.maybeOf(context)?.accessibleNavigation, preserving the existing false
fallback and keyboard-dismissal behavior.

In `@math_keyboard/lib/src/widgets/math_keyboard.dart`:
- Around line 900-921: Replace the traversal-order literals in the keyboard
layout with named constants for the numbers, variables, functions, and submit
range bases. Initialize numberKeyOrder and functionKeyOrder from the
corresponding constants, and use the same constants for variable and submit
ordering in the related section, making the 100-key range boundaries explicit.

In `@math_keyboard/test/widgets/math_cursor_context_test.dart`:
- Around line 10-65: Add addTearDown(controller.dispose) to each of the six
tests after creating the MathFieldEditingController, including the controllers
configured with leaves or functions. Keep the existing assertions and controller
setup unchanged.

In `@math_keyboard/test/widgets/math_keyboard_a11y_test.dart`:
- Around line 302-313: Rename the local variable `german` in the
`MathKeyboardSemantics override` test to `base` or `english`, since it
references `MathKeyboardSemantics.fallback` rather than a German localization;
update its subsequent `copyWith` usage accordingly.

In `@math_keyboard/test/widgets/math_keyboard_focus_test.dart`:
- Around line 91-156: The existing focus traversal test only verifies forward
exit; add a backward-boundary test near the current tab traversal coverage that
tabs from the MathField into the keyboard once, then sends Shift+Tab and pumps
focus updates. Assert that the MathField regains focus and no KeyboardButton
remains focused, confirming PreviousFocusIntent uses onExitToField rather than
wrapping to the last key.

In `@math_keyboard/test/widgets/math_keyboard_screen_reader_test.dart`:
- Around line 61-64: In the screen-reader test, replace the deprecated
tester.binding.pipelineOwner.semanticsOwner access and the intermediate node
from getSemantics with tester.semantics.performAction targeting
find.bySemanticsLabel('Math field') and SemanticsAction.tap; remove the
deprecation suppression.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e988c140-8c69-4fa0-98a1-b8919e7cc04d

📥 Commits

Reviewing files that changed from the base of the PR and between 3a416d4 and aa9e92a.

📒 Files selected for processing (17)
  • math_keyboard/CHANGELOG.md
  • math_keyboard/README.md
  • math_keyboard/lib/math_keyboard.dart
  • math_keyboard/lib/src/foundation/keyboard_button.dart
  • math_keyboard/lib/src/foundation/math_keyboard_semantics.dart
  • math_keyboard/lib/src/widgets/keyboard_button.dart
  • math_keyboard/lib/src/widgets/math_field.dart
  • math_keyboard/lib/src/widgets/math_form_field.dart
  • math_keyboard/lib/src/widgets/math_keyboard.dart
  • math_keyboard/lib/src/widgets/math_keyboard_theme.dart
  • math_keyboard/pubspec.yaml
  • math_keyboard/test/widgets/math_cursor_context_test.dart
  • math_keyboard/test/widgets/math_keyboard_a11y_test.dart
  • math_keyboard/test/widgets/math_keyboard_focus_test.dart
  • math_keyboard/test/widgets/math_keyboard_screen_reader_test.dart
  • math_keyboard/test/widgets/math_keyboard_semantics_group_test.dart
  • math_keyboard/test/widgets/math_keyboard_test.dart

Comment on lines +341 to +370
int get hashCode => Object.hashAll([
deleteLabel,
submitLabel,
showNumbersKeyboardLabel,
showFunctionsKeyboardLabel,
keyboardGroupLabel,
variablesGroupLabel,
functionsGroupLabel,
numbersGroupLabel,
moveCursorLeftLabel,
moveCursorRightLabel,
mathFieldLabel,
emptyLabel,
expressionContainerLabel,
numeratorLabel,
denominatorLabel,
exponentLabel,
rootIndexLabel,
underRootLabel,
underSquareRootLabel,
logBaseLabel,
logArgumentLabel,
insideParenthesesLabel,
variableLabel,
beforeToken,
startOfContainer,
endOfContainer,
Object.hashAll(functionMappings),
Object.hashAll(
tokenMappings.entries.map((e) => Object.hash(e.key, e.value))),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For Dart SDK 3.9, verify that Object.hashAllUnordered is available and is appropriate for hashing a Map's entries when equality is insertion-order independent.

💡 Result:

Object.hashAllUnordered is a long-standing member of the dart:core library, available in Dart SDK 3.9 and all stable versions since its introduction in Dart 2.14 [1][2][3]. It is appropriate to use Object.hashAllUnordered for hashing a Map's entries when equality is insertion-order independent [1][4]. Because the function computes a hash code by combining the hash codes of its input objects in an order-independent manner, it is well-suited for collections like Sets or Maps where the identity of the data does not depend on the specific sequence of the elements or entries [1][2][5]. When using this to hash a Map, you should pass the collection of Map entries (e.g., map.entries) to Object.hashAllUnordered [1]. Ensure that your equality logic also correctly handles unordered comparisons to maintain the required contract where equal objects must have identical hash codes [6][4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
file="$(fd -t f 'math_keyboard_semantics\.dart$' . | head -n 1)"
printf '%s\n' "FILE=$file"
sed -n '300,380p' "$file"
printf '%s\n' '--- equality and mapping references ---'
rg -n -C 4 'operator ==|mapEquals|tokenMappings|functionMappings|hashAllUnordered|environment|sdk' "$file" pubspec.yaml math_keyboard/pubspec.yaml 2>/dev/null || true

Repository: simpleclub/math_keyboard

Length of output: 15429


Use an order-independent hash for tokenMappings.

mapEquals ignores insertion order, but the current hash depends on tokenMappings.entries iteration order. Replace Object.hashAll with Object.hashAllUnordered for these entry hashes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/foundation/math_keyboard_semantics.dart` around lines
341 - 370, The hashCode implementation for the semantics class must match
mapEquals’ order-independent behavior. In the hash list near functionMappings
and tokenMappings, replace the ordered hashing of tokenMappings.entries with
Object.hashAllUnordered while preserving the existing entry key/value hashes.

Comment on lines +314 to 330
if (open) {
// Escape returns focus to the field but must leave the keyboard closed,
// so honor a one-shot suppression instead of reopening.
if (_suppressReopen) {
_suppressReopen = false;
setState(() {});
return;
}
// Guard against re-opening while already shown: focus can return to the
// field from a key (e.g. via shift+tab), and re-inserting the overlay
// would restart the slide and drop the key focus.
if (!_isKeyboardShown) {
_openKeyboard(context);
_keyboardSlideController.forward(from: 0);
}
_cursorBlinkController.repeat();

_showFieldOnScreen();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The caret stops blinking after Escape while the field keeps focus.

The _suppressReopen branch returns before _cursorBlinkController.repeat(). _handleFocusChanged(open: false) previously set the controller to 1 / 2, and _handleBlinkUpdate maps that value to opacity 0. After Escape the field holds focus but shows no caret, so the user loses the focus indication until the next tap.

🐛 Proposed fix
       if (_suppressReopen) {
         _suppressReopen = false;
+        // The field keeps focus, so the caret must stay visible.
+        _cursorBlinkController.repeat();
         setState(() {});
         return;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (open) {
// Escape returns focus to the field but must leave the keyboard closed,
// so honor a one-shot suppression instead of reopening.
if (_suppressReopen) {
_suppressReopen = false;
setState(() {});
return;
}
// Guard against re-opening while already shown: focus can return to the
// field from a key (e.g. via shift+tab), and re-inserting the overlay
// would restart the slide and drop the key focus.
if (!_isKeyboardShown) {
_openKeyboard(context);
_keyboardSlideController.forward(from: 0);
}
_cursorBlinkController.repeat();
_showFieldOnScreen();
if (open) {
// Escape returns focus to the field but must leave the keyboard closed,
// so honor a one-shot suppression instead of reopening.
if (_suppressReopen) {
_suppressReopen = false;
// The field keeps focus, so the caret must stay visible.
_cursorBlinkController.repeat();
setState(() {});
return;
}
// Guard against re-opening while already shown: focus can return to the
// field from a key (e.g. via shift+tab), and re-inserting the overlay
// would restart the slide and drop the key focus.
if (!_isKeyboardShown) {
_openKeyboard(context);
_keyboardSlideController.forward(from: 0);
}
_cursorBlinkController.repeat();
_showFieldOnScreen();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_field.dart` around lines 314 - 330, Update
the _suppressReopen branch in the focus-change handler so it restarts
_cursorBlinkController before returning, while preserving the one-shot
suppression and state update behavior.

Comment on lines +835 to +846
return Semantics(
textField: true,
label: _semanticsLabel,
value: controller.readableExpression(semantics),
onTap: onTap,
child: ExcludeSemantics(child: field),
);
}

/// The accessibility label describing the field itself.
String get _semanticsLabel =>
decoration.labelText ?? decoration.hintText ?? semantics.mathFieldLabel;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

ExcludeSemantics hides the validation error from screen readers.

The wrapper excludes the whole InputDecorator subtree, which includes decoration.errorText. MathFormField sets errorText from the validator (see math_keyboard/lib/src/widgets/math_form_field.dart), so a failed validation is now rendered visually but is absent from the semantics tree. A screen-reader user receives no validation feedback.

Expose the error on the outer node.

♿ Proposed fix
     return Semantics(
       textField: true,
       label: _semanticsLabel,
       value: controller.readableExpression(semantics),
+      // The decorator subtree is excluded, so the validation error has to be
+      // carried by this node.
+      hint: decoration.errorText,
       onTap: onTap,
       child: ExcludeSemantics(child: field),
     );

Add a test that asserts the error text reaches the semantics node.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return Semantics(
textField: true,
label: _semanticsLabel,
value: controller.readableExpression(semantics),
onTap: onTap,
child: ExcludeSemantics(child: field),
);
}
/// The accessibility label describing the field itself.
String get _semanticsLabel =>
decoration.labelText ?? decoration.hintText ?? semantics.mathFieldLabel;
return Semantics(
textField: true,
label: _semanticsLabel,
value: controller.readableExpression(semantics),
// The decorator subtree is excluded, so the validation error has to be
// carried by this node.
hint: decoration.errorText,
onTap: onTap,
child: ExcludeSemantics(child: field),
);
}
/// The accessibility label describing the field itself.
String get _semanticsLabel =>
decoration.labelText ?? decoration.hintText ?? semantics.mathFieldLabel;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_field.dart` around lines 835 - 846, Update
the Semantics wrapper in the math field widget to expose the current
decoration.errorText on the outer semantics node while preserving the existing
ExcludeSemantics behavior for the child InputDecorator. Add a widget
accessibility test covering MathFormField validation and assert that the
validator’s error text is present in the resulting semantics node.

Comment on lines +118 to +137
final style = this.style ?? MathKeyboardTheme.styleOf(context);
final semantics = this.semantics ?? MathKeyboardTheme.semanticsOf(context);
final scaler = MediaQuery.textScalerOf(context);
final textScale = (scaler.scale(style.baseFontSize) / style.baseFontSize)
.clamp(1.0, style.maxTextScaleFactor);
final fontSize = style.baseFontSize * textScale;
// Keys default to a fixed size (maxTextScaleFactor == 1): large-text
// accessibility is provided by the large-content-viewer (long-press a key
// to magnify it), so the layout does not need to reflow. A consumer can
// instead opt into resize (WCAG 1.4.4) by raising maxTextScaleFactor, in
// which case keys grow up to keyHeight * maxTextScaleFactor. The FittedBox
// on each label stays as a safety net against overflow.
final keyHeight = style.keyHeight * textScale;
// In landscape the expression keyboard shows the functions and numbers side
// by side with a full-height submit key, instead of paging. The number-only
// keyboard keeps its compact paged layout.
final isLandscape =
type != MathKeyboardType.numberOnly &&
MediaQuery.orientationOf(context) == Orientation.landscape;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against a maxTextScaleFactor below 1.

clamp(1.0, style.maxTextScaleFactor) throws ArgumentError when maxTextScaleFactor is less than 1.0. MathKeyboardStyle accepts any double for this field, so a consumer can reach this crash through copyWith(maxTextScaleFactor: 0.8). Add an assertion on the field in MathKeyboardStyle (in math_keyboard/lib/src/widgets/math_keyboard_theme.dart), or clamp the upper limit here.

🛡️ Defensive fix at the consumption point
-    final textScale = (scaler.scale(style.baseFontSize) / style.baseFontSize)
-        .clamp(1.0, style.maxTextScaleFactor);
+    final maxTextScale = math.max(1.0, style.maxTextScaleFactor);
+    final textScale = (scaler.scale(style.baseFontSize) / style.baseFontSize)
+        .clamp(1.0, maxTextScale);

The preferred fix is an assertion in the MathKeyboardStyle constructor so the misconfiguration is reported at its source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_keyboard.dart` around lines 118 - 137, Add
an assertion in the MathKeyboardStyle constructor requiring maxTextScaleFactor
to be at least 1.0, so invalid values such as those passed through copyWith are
rejected at configuration time and the existing clamp in the keyboard layout
remains safe.

Comment on lines +636 to +639
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
final layout = controller.secondPage ? page2! : page1 ?? numberKeyboard;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the page2! force-unwrap.

For MathKeyboardType.numberOnly, _buildPortrait passes page2: null. MathFieldEditingController.togglePage() is public and flips secondPage for any keyboard type, so a consumer can set secondPage to true on a number-only field. This build then throws a null-check error.

🛡️ Proposed guard
-        final layout = controller.secondPage ? page2! : page1 ?? numberKeyboard;
+        final layout = (controller.secondPage ? page2 : page1) ?? numberKeyboard;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
final layout = controller.secondPage ? page2! : page1 ?? numberKeyboard;
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
final layout = (controller.secondPage ? page2 : page1) ?? numberKeyboard;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_keyboard.dart` around lines 636 - 639,
Guard the page selection in _buildPortrait so controller.secondPage cannot
force-unwrap a null page2. When page2 is unavailable, including number-only
keyboards with secondPage set to true, fall back to page1 or numberKeyboard
while preserving page2 selection when it exists.

Comment on lines +1059 to 1066
@override
Widget build(BuildContext context) {
final resolvedSemanticsLabel =
semanticsLabel ??
(asTex
? semantics.functionLabel(label!)
: (label == '.' ? decimalSeparator(context) : label));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect tokenLabel and its mappings to confirm the fallback behavior.
fd -t f 'math_keyboard_semantics.dart' | while IFS= read -r f; do
  rg -n -C 6 'tokenLabel|tokenMappings' "$f"
done

Repository: simpleclub/math_keyboard

Length of output: 5010


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- math_keyboard.dart ---'
rg -n -C 12 'resolvedSemanticsLabel|class .*Key|final String\?? label|asTex' math_keyboard/lib/src/widgets/math_keyboard.dart
printf '%s\n' '--- math_keyboard_semantics.dart mappings ---'
rg -n -C 8 '_defaultTokenMappings|tokenMappings|[+×−=]' math_keyboard/lib/src/widgets/math_keyboard_semantics.dart
printf '%s\n' '--- math_field.dart reader ---'
rg -n -C 12 '_readNode|tokenLabel' math_keyboard/lib/src/widgets/math_field.dart

Repository: simpleclub/math_keyboard

Length of output: 8288


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate semantics files ---'
fd -t f -i 'semantics' math_keyboard
printf '%s\n' '--- math_field files ---'
fd -t f -i 'math_field' math_keyboard
printf '%s\n' '--- tokenLabel definitions and uses ---'
rg -n -C 8 'tokenLabel|_defaultTokenMappings|tokenMappings' math_keyboard

Repository: simpleclub/math_keyboard

Length of output: 20492


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- button configuration definitions ---'
rg -n -C 10 'BasicKeyboardButtonConfig|KeyboardButtonConfig|label:|value:' math_keyboard/lib/src/widgets/math_keyboard.dart
printf '%s\n' '--- token-related tests ---'
rg -n -C 8 'tokenLabel|readableExpression|semanticsLabel|semantic' math_keyboard/test

Repository: simpleclub/math_keyboard

Length of output: 23296


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- configuration classes and keyboard layouts ---'
rg -n -C 12 'class (BasicKeyboardButtonConfig|KeyboardButtonConfig)|BasicKeyboardButtonConfig\(|numberKeyboard|page1|page2|\\cdot|\\div|[×−]' math_keyboard/lib
printf '%s\n' '--- relevant accessibility assertions ---'
sed -n '186,250p' math_keyboard/test/widgets/math_keyboard_a11y_test.dart
sed -n '251,345p' math_keyboard/test/widgets/math_keyboard_a11y_test.dart

Repository: simpleclub/math_keyboard

Length of output: 48591


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
import re
from pathlib import Path

semantics = Path("math_keyboard/lib/src/foundation/math_keyboard_semantics.dart").read_text()
keyboard = Path("math_keyboard/lib/src/foundation/keyboard_button.dart").read_text()

mapping_block = re.search(
    r"_defaultTokenMappings\s*=\s*\{(.*?)\n\s*\};", semantics, re.S
).group(1)
mapping = dict(re.findall(r"\s*(r?'[^']*'|'[^']*')\s*:\s*'([^']*)'", mapping_block))
mapping = {k[1:] if k.startswith("r'") else k[1:]: v for k, v in mapping.items()}

pairs = re.findall(
    r"label:\s*(r?'[^']*'|'[^']*'|\S+),\s*"
    r"value:\s*(r?'[^']*'|'[^']*'|\S+)",
    keyboard,
)
def decode(token):
    if token.startswith(("r'", "'")) and token.endswith("'"):
        return token[1:-1]
    return token.rstrip(",")

def token_label(token):
    return mapping.get(token, token.replace("{", "").replace("}", ""))

for raw_label, raw_value in pairs:
    label, value = decode(raw_label), decode(raw_value)
    if label in {"×", "÷", "−", "+", "(", ")"}:
        print({
            "label": label,
            "value": value,
            "tokenLabel(label)": token_label(label),
            "tokenLabel(value)": token_label(value),
        })
PY

Repository: simpleclub/math_keyboard

Length of output: 672


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
import re
from pathlib import Path

semantics = Path("math_keyboard/lib/src/foundation/math_keyboard_semantics.dart").read_text()
keyboard = Path("math_keyboard/lib/src/foundation/keyboard_button.dart").read_text()

mapping_block = re.search(
    r"_defaultTokenMappings\s*=\s*\{(.*?)\n\s*\};", semantics, re.S
).group(1)
mapping = {}
for key, value in re.findall(r"\s*(r?'[^']*'|'[^']*')\s*:\s*'([^']*)'", mapping_block):
    mapping[key[2:-1] if key.startswith("r'") else key[1:-1]] = value

pairs = re.findall(
    r"label:\s*(r?'[^']*'|'[^']*'|\S+),\s*"
    r"value:\s*(r?'[^']*'|'[^']*'|\S+)",
    keyboard,
)

def decode(token):
    if token.startswith("r'") and token.endswith("'"):
        return token[2:-1]
    if token.startswith("'") and token.endswith("'"):
        return token[1:-1]
    return token.rstrip(",")

def token_label(token):
    return mapping.get(token, token.replace("{", "").replace("}", ""))

for raw_label, raw_value in pairs:
    label, value = decode(raw_label), decode(raw_value)
    if label in {"×", "÷", "−", "+", "(", ")"}:
        print(f"{label!r}: value={value!r}, "
              f"tokenLabel(label)={token_label(label)!r}, "
              f"tokenLabel(value)={token_label(value)!r}")
PY

Repository: simpleclub/math_keyboard

Length of output: 619


Resolve plain-text key semantics from the inserted token.

The displayed × and labels are not keys in tokenMappings; their inserted values are \cdot and -. Pass BasicKeyboardButtonConfig.value to _BasicButton, then call semantics.tokenLabel on that token. Add a mapping for \frac or provide a dedicated division label. Unmapped digits remain unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@math_keyboard/lib/src/widgets/math_keyboard.dart` around lines 1059 - 1066,
Update the basic key semantics flow around BasicKeyboardButtonConfig and
_BasicButton to pass the button value and resolve plain-text labels with
semantics.tokenLabel(value) instead of the displayed label. Ensure \frac
receives a token mapping or dedicated division label, while unmapped digit
semantics remain unchanged.

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.00253% with 174 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.51%. Comparing base (80cbf1a) to head (9f454d9).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
...rd/lib/src/foundation/math_keyboard_semantics.dart 36.79% 67 Missing ⚠️
..._keyboard/lib/src/widgets/math_keyboard_theme.dart 39.04% 64 Missing ⚠️
math_keyboard/lib/src/widgets/math_field.dart 86.86% 18 Missing ⚠️
math_keyboard/lib/src/widgets/math_form_field.dart 0.00% 8 Missing ⚠️
math_keyboard/lib/src/foundation/tex2math.dart 87.75% 6 Missing ⚠️
math_keyboard/lib/src/widgets/math_keyboard.dart 98.37% 5 Missing ⚠️
math_keyboard/lib/src/widgets/keyboard_button.dart 91.66% 4 Missing ⚠️
math_keyboard/lib/src/widgets/view_insets.dart 0.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main      #90       +/-   ##
===========================================
+ Coverage   27.11%   72.51%   +45.40%     
===========================================
  Files          11       13        +2     
  Lines        1147     1692      +545     
===========================================
+ Hits          311     1227      +916     
+ Misses        836      465      -371     
Flag Coverage Δ
math_keyboard 72.51% <78.00%> (+45.40%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread math_keyboard/example/ios/Flutter/ephemeral/flutter_lldb_helper.py Outdated
Comment thread math_keyboard/example/ios/Flutter/ephemeral/flutter_lldbinit Outdated
Comment thread math_keyboard/pubspec.yaml
Comment thread math_keyboard/pubspec.yaml
@MaElaRo
MaElaRo requested a review from edhom August 10, 2026 13:50
Comment thread math_keyboard/lib/src/widgets/math_keyboard.dart
…eading

- MathField/MathFormField gain an optional `semanticsValue` that overrides
  the built-in expression linearization, so a consumer can inject speech from
  a dedicated TeX-to-speech engine. When null, the field falls back to
  `readableExpression`, keeping the package dependency-free and accessible on
  its own.
- tokenLabel now speaks function-like leaves (e.g. the trig keys' `\cos(`) as
  words and strips any remaining TeX control chars, so a screen reader never
  reads out a raw backslash.
- Guard the text-scale clamp with `math.max` so a sub-1 maxTextScaleFactor
  can't throw in release builds (asserts stripped).
- A stray `togglePage()` on a number-only keyboard keeps the numbers layout
  and its region label in agreement (no null `page2` unwrap, no mislabel).
- Skip the field's auto-scroll when the scroll view isn't attached, avoiding
  a "ScrollController not attached" assertion on rebuild.
The key tier now derives from asTex/isDigit, so BasicKeyboardButtonConfig.highlighted was read nowhere; the config class is unexported, so this has no consumer impact.
A bare MathKeyboard contains tab focus in its keys; focus only leaves via onExitToField/onExitToNext, which need focusScopeNode to work. Document that on the class and assert callbacks are never supplied without a scope (the silent case that never fires).

@edhom edhom left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Love to see the math keyboard becoming accessible!

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Change keyboard primary button color A RenderLine overflowed by 1.5 pixels on the right.

3 participants