diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3c39f0a9..ae615ba0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -54,6 +54,13 @@ You can always edit this file by hand instead — the helpers just save effort. ### Fixed +- **Buttons a screen reader announced but could not press now press.** The + idiom that reads a tappable row as one announcement dropped the tap along + with the text it hid, leaving twenty-seven controls flagged as buttons with + no action to carry — the drill and lesson rows, the studio and appearance + pickers, the flashcard, the pronounce button and the welcome sound toggle + among them. A guard now reads the sources and fails if the pair comes apart. + - **A half-translated language folder is refused, not shown.** The fallback to English reaches every depth of an entry, so a folder can translate one of a lesson's cards or one of a help entry's steps and leave the rest in English. diff --git a/lib/core/widgets/bordered_tap_row.dart b/lib/core/widgets/bordered_tap_row.dart index 0cd9dbf5..40b3d2b8 100644 --- a/lib/core/widgets/bordered_tap_row.dart +++ b/lib/core/widgets/bordered_tap_row.dart @@ -5,11 +5,9 @@ import 'package:flutter/material.dart'; /// A surface on the ground, ruled and tappable: the frame the design's chips /// and slim entry rows share. /// -/// Only the frame. What goes inside is the caller's — a chip pairs a name with -/// a count, a row pairs a name with a chevron — and the shapes stay separate -/// components because the design draws them as two. What they must not do is -/// disagree about the surface, the rule and the corner they sit on, which is -/// the part that lives here. +/// Only the frame; what goes inside is the caller's. The chip and the row stay +/// separate components because the design draws them as two, but they must not +/// disagree about the surface, the rule and the corner, which live here. class BorderedTapRow extends StatelessWidget { /// Creates a [BorderedTapRow]. const BorderedTapRow({ @@ -40,6 +38,10 @@ class BorderedTapRow extends StatelessWidget { return Semantics( button: true, label: semanticsLabel, + // The action, not just the flag: `excludeSemantics` drops the InkWell's + // tap along with the text, and a node saying "button" with no tap to + // carry announces something a screen reader cannot press (#487). + onTap: onTap, excludeSemantics: true, child: Material( color: mood.surface, diff --git a/lib/core/widgets/disclosure.dart b/lib/core/widgets/disclosure.dart index 1caceef6..51a5920b 100644 --- a/lib/core/widgets/disclosure.dart +++ b/lib/core/widgets/disclosure.dart @@ -132,11 +132,17 @@ class Disclosure extends StatelessWidget { } if (onToggle == null) return row; + // The tap is carried here only when the child's own is being dropped with + // its text (#487). Beside a live InkWell it would split the row into two + // nodes, taking the button flag off the one that carries the label. + final announcesAlone = semanticsLabel != null; + return Semantics( button: true, expanded: collapsible ? isOpen : null, label: semanticsLabel, - excludeSemantics: semanticsLabel != null, + onTap: announcesAlone ? onToggle : null, + excludeSemantics: announcesAlone, child: InkWell(onTap: onToggle, child: row), ); } diff --git a/lib/core/widgets/reward_row.dart b/lib/core/widgets/reward_row.dart index 1f78bd13..a2afd209 100644 --- a/lib/core/widgets/reward_row.dart +++ b/lib/core/widgets/reward_row.dart @@ -9,14 +9,9 @@ import 'package:flutter/material.dart'; /// One occasional beat on a reward screen: what happened, and a quiet line /// saying what it means. /// -/// **The one anatomy for every reward-list row** — a label, a muted detail, -/// and at most one trailing affordance. The freeze earn, a new card and the -/// Coffee Challenge offer are all this shape, which is what makes them read as -/// one list rather than three unrelated announcements. -/// -/// Deliberately plain: no well, no accent kicker, no card of its own. The -/// screen already carries the celebration; a row that decorated itself would -/// compete with the tree above it. +/// The one anatomy for every reward-list row — a label, a muted detail and at +/// most one trailing affordance — so the freeze earn, a new card and the +/// Coffee Challenge offer read as one list rather than three announcements. class RewardRow extends StatelessWidget { /// Creates a [RewardRow]. const RewardRow({ @@ -98,6 +93,7 @@ class RewardRow extends StatelessWidget { return Semantics( button: true, label: detail == null ? label : '$label. $detail', + onTap: onPress, excludeSemantics: true, child: InkWell(onTap: onPress, child: row), ); diff --git a/lib/features/challenges/presentation/challenge_offer_row.dart b/lib/features/challenges/presentation/challenge_offer_row.dart index 12a50644..24c19fc1 100644 --- a/lib/features/challenges/presentation/challenge_offer_row.dart +++ b/lib/features/challenges/presentation/challenge_offer_row.dart @@ -11,22 +11,10 @@ import 'package:flutter/material.dart'; /// The Coffee Challenge offer, as one row of a reward screen. /// -/// The app's port of the design's `ChallengeSuggestion`. It is not the only -/// one yet: the lesson ending still runs [ChallengeSuggestion], an older -/// bordered card with its own *Save for later*, and converging the two onto -/// this row is #490's job rather than this one's. Until then the confirmation -/// sentence and the write exist in both places, deliberately. -/// -/// **One affordance, and no way to say no.** The design gives the row a go -/// button and nothing else: declining is walking past it, because the challenge -/// waits on the Path either way. That is why there is no *not now* here — a -/// dismissal would have to mean something, and there is nothing for it to mean. -/// -/// **The row is the button**, not the circle inside it — the anatomy every -/// actionable reward row shares (*"Span, not button: the row itself is the -/// button"*). The design's own offer wires the tap to the 38-px circle alone, -/// which is under every platform's minimum touch target; the row is well over -/// it and the circle still reads as the affordance. +/// The row is the button, not the 38-px circle inside it: the design wires the +/// tap to that circle alone, which is under every platform's minimum touch +/// target. The lesson ending still runs the older [ChallengeSuggestion] card, +/// and converging the two onto this row is #490's job rather than this one's. class ChallengeOfferRow extends StatelessWidget { /// Creates a [ChallengeOfferRow]. const ChallengeOfferRow({ @@ -74,6 +62,7 @@ class ChallengeOfferRow extends StatelessWidget { return Semantics( button: true, label: '$kicker. $_detail. $startAction', + onTap: onStart, excludeSemantics: true, child: InkWell( onTap: onStart, diff --git a/lib/features/challenges/presentation/challenge_reaction_chips.dart b/lib/features/challenges/presentation/challenge_reaction_chips.dart index d5ca8215..b173b356 100644 --- a/lib/features/challenges/presentation/challenge_reaction_chips.dart +++ b/lib/features/challenges/presentation/challenge_reaction_chips.dart @@ -73,6 +73,7 @@ class _Chip extends StatelessWidget { button: true, selected: selected, label: label, + onTap: onTap, excludeSemantics: true, child: InkWell( borderRadius: BorderRadius.circular(AppRadii.pill), diff --git a/lib/features/companion/presentation/roasty_moment.dart b/lib/features/companion/presentation/roasty_moment.dart index cc4c346d..881c772e 100644 --- a/lib/features/companion/presentation/roasty_moment.dart +++ b/lib/features/companion/presentation/roasty_moment.dart @@ -11,15 +11,9 @@ import 'package:flutter/material.dart'; /// The full-screen beat a reward route opens on: the companion, an eyebrow, a /// headline, and a bar that runs down to the content behind it. /// -/// **A beat, not a screen.** It owns no content and no navigation — it holds -/// the frame for [hold], then hands over through [onDone]. Every reward route -/// in the design opens on one, so it is built shared rather than by whichever -/// route landed first. -/// -/// ⚠️ **[onDone] fires exactly once, and fires under reduced motion too.** -/// A tap and the timer race by construction, and a host that sequences its -/// screen behind this callback stalls forever if stillness swallows it — the -/// same discipline the tree's growth is held to (ADR-0011). +/// A beat, not a screen: it owns no content and no navigation, holding the +/// frame for [hold] and then handing over through [onDone]. Every reward route +/// in the design opens on one, so it is built shared. class RoastyMoment extends StatefulWidget { /// Creates a [RoastyMoment]. const RoastyMoment({ @@ -47,7 +41,9 @@ class RoastyMoment extends StatefulWidget { /// The headline, which varies with what the run actually did. final String title; - /// Called once, when the beat is over — by the timer or by a tap. + /// Called once, when the beat is over — by the timer or by a tap, and under + /// reduced motion too, so a host sequencing behind it never stalls + /// (ADR-0011). final VoidCallback onDone; /// The companion's rendered size in the beat. @@ -121,6 +117,7 @@ class _RoastyMomentState extends State button: true, label: '${widget.eyebrow}. ${widget.title}', hint: 'Tap to continue', + onTap: _finish, excludeSemantics: true, child: Stack( children: [ diff --git a/lib/features/dictionary/presentation/category_index.dart b/lib/features/dictionary/presentation/category_index.dart index 6cea4d3a..0fd6aca4 100644 --- a/lib/features/dictionary/presentation/category_index.dart +++ b/lib/features/dictionary/presentation/category_index.dart @@ -81,6 +81,7 @@ class _CategoryRow extends StatelessWidget { return Semantics( button: true, label: '${category.label}, $count terms. ${category.summary}', + onTap: onOpen, excludeSemantics: true, child: InkWell( onTap: onOpen, diff --git a/lib/features/dictionary/presentation/dictionary_quick_chips.dart b/lib/features/dictionary/presentation/dictionary_quick_chips.dart index 545ec5f2..5839c353 100644 --- a/lib/features/dictionary/presentation/dictionary_quick_chips.dart +++ b/lib/features/dictionary/presentation/dictionary_quick_chips.dart @@ -18,10 +18,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// The drill row on Dictionary home — one slim chip per practice surface the /// dictionary owns. /// -/// Both chips now (#97, #98), in the design's own order: Flashcards leads -/// because it drills what the learner chose to keep, and only it carries a -/// count — the design gives *Guess the term* none, since the whole glossary -/// is not a number worth reading. +/// In the design's own order: Flashcards leads because it drills what the +/// learner chose to keep, and only it carries a count — the design gives +/// *Guess the term* none (#97, #98). class DictionaryQuickChips extends ConsumerWidget { /// Creates a [DictionaryQuickChips]. const DictionaryQuickChips({super.key}); @@ -56,19 +55,21 @@ class _FlashcardsChip extends StatelessWidget { @override Widget build(BuildContext context) { final mood = context.mood; + void open() => unawaited(context.pushActivity(flashcardReview)); return Semantics( button: true, label: cards == null ? FlashcardsCopy.title : '${FlashcardsCopy.title}, ${FlashcardsCopy.deckLine(cards!)}', + onTap: open, excludeSemantics: true, child: Material( color: mood.surface, borderRadius: BorderRadius.circular(AppRadii.chrome), child: InkWell( borderRadius: BorderRadius.circular(AppRadii.chrome), - onTap: () => unawaited(context.pushActivity(flashcardReview)), + onTap: open, child: Container( decoration: BoxDecoration( border: Border.all(color: mood.rule), @@ -116,18 +117,20 @@ class _VocabChip extends StatelessWidget { @override Widget build(BuildContext context) { final mood = context.mood; + void open() => unawaited(context.pushActivity(vocabGame)); return Semantics( button: true, label: VocabCopy.title, hint: VocabCopy.rowSubtitle, + onTap: open, excludeSemantics: true, child: Material( color: mood.surface, borderRadius: BorderRadius.circular(AppRadii.chrome), child: InkWell( borderRadius: BorderRadius.circular(AppRadii.chrome), - onTap: () => unawaited(context.pushActivity(vocabGame)), + onTap: open, child: Container( decoration: BoxDecoration( border: Border.all(color: mood.rule), diff --git a/lib/features/dictionary/presentation/flashcard_view.dart b/lib/features/dictionary/presentation/flashcard_view.dart index 856e1a5a..16df1cdf 100644 --- a/lib/features/dictionary/presentation/flashcard_view.dart +++ b/lib/features/dictionary/presentation/flashcard_view.dart @@ -104,6 +104,7 @@ class _FlashcardViewState extends State hint: widget.isRevealed ? FlashcardsCopy.tapToSeeTerm : FlashcardsCopy.tapToReveal, + onTap: widget.onFlip, excludeSemantics: true, child: GestureDetector( onTap: widget.onFlip, diff --git a/lib/features/dictionary/presentation/speak_button.dart b/lib/features/dictionary/presentation/speak_button.dart index 5529f8bd..1aa341c4 100644 --- a/lib/features/dictionary/presentation/speak_button.dart +++ b/lib/features/dictionary/presentation/speak_button.dart @@ -96,6 +96,7 @@ class _SpeakChipState extends ConsumerState<_SpeakChip> { return Semantics( button: true, label: 'Pronounce ${widget.word}', + onTap: _speak, excludeSemantics: true, child: InkWell( onTap: _speak, diff --git a/lib/features/dictionary/presentation/term_entry_body.dart b/lib/features/dictionary/presentation/term_entry_body.dart index 504488f5..4d7d24a4 100644 --- a/lib/features/dictionary/presentation/term_entry_body.dart +++ b/lib/features/dictionary/presentation/term_entry_body.dart @@ -242,17 +242,22 @@ class _PathBlock extends ConsumerWidget { // Until the place resolves there is nothing honest to show — an id is not // an answer, so the row simply has no text yet. final title = place?.title ?? ''; + final open = id == null + ? null + : () => unawaited(context.pushActivity(lessonRun(id))); return _Block( label: status.pathLabel, child: Semantics( - button: true, + // Only a button once there is an id to open: before the place + // resolves there is nothing to press, and saying otherwise is the + // announcement this row is being fixed for (#487). + button: open != null, label: '$title, ${accessible ? 'opens the lesson' : 'locked'}', + onTap: open, excludeSemantics: true, child: InkWell( - onTap: id == null - ? null - : () => unawaited(context.pushActivity(lessonRun(id))), + onTap: open, child: Row( children: [ ClipRRect( diff --git a/lib/features/dictionary/presentation/term_full_entry_gate.dart b/lib/features/dictionary/presentation/term_full_entry_gate.dart index e92f1b02..f13a713e 100644 --- a/lib/features/dictionary/presentation/term_full_entry_gate.dart +++ b/lib/features/dictionary/presentation/term_full_entry_gate.dart @@ -13,18 +13,10 @@ import 'package:flutter/material.dart'; /// Where the full entry would be, for a learner without the course. /// -/// The deep explanation, the example, the self-check and the sources come -/// with the course (`docs/decisions.md` §12), so the entry stops at its short -/// explanation and this row stands in for the rest. It is the offer at the -/// moment the learner wants more — ADR-0005's *"targeted course pitch at peak -/// intent"* — and it says what would open it rather than only that it is -/// shut (ADR-0016). -/// -/// **The label promises the full entry**, so the tap raises the gate; it must -/// never deliver the short explanation they are already reading. -/// -/// One lock, drawn in accent, because it is a purchase lock and not a -/// progression one — ADR-0016's rule for every locked row. +/// The deep explanation, example, self-check and sources come with the course +/// (`docs/decisions.md` §12), so the entry stops at its short explanation and +/// this row stands in — saying what would open it, in accent because it is a +/// purchase lock rather than a progression one (ADR-0016). class TermFullEntryGate extends StatelessWidget { /// Creates a [TermFullEntryGate] for the term called [term]. const TermFullEntryGate({required this.term, super.key}); @@ -38,14 +30,16 @@ class TermFullEntryGate extends StatelessWidget { @override Widget build(BuildContext context) { final mood = context.mood; + void openGate() => + unawaited(showPlusGate(context, LockedFullEntry(term: term))); return Semantics( button: true, label: TermEntryCopy.gateSemantics, + onTap: openGate, excludeSemantics: true, child: InkWell( - onTap: () => - unawaited(showPlusGate(context, LockedFullEntry(term: term))), + onTap: openGate, borderRadius: BorderRadius.circular(AppRadii.chrome), child: Container( padding: const EdgeInsets.all(AppSpacing.base), diff --git a/lib/features/learn/presentation/practice/replay_row.dart b/lib/features/learn/presentation/practice/replay_row.dart index fbc8c508..153616e0 100644 --- a/lib/features/learn/presentation/practice/replay_row.dart +++ b/lib/features/learn/presentation/practice/replay_row.dart @@ -12,16 +12,9 @@ import 'package:flutter/material.dart'; /// One row of the practice shelf: what it drills, its name, what it costs or /// takes, and the replay mark — or a lock. /// -/// **The row is not a card.** The design draws `.tap-row` flat on the page, -/// with a press highlight and nothing else around it; the shelf reads as one -/// list, not as a stack of boxes. The highlight bleeds a stop past the text -/// on each side (`margin: 0 -8px; padding: 12px 8px`), which is why the shelf -/// sits that stop inside the page gutter and every row pads it back. -/// -/// Every practice list draws this one row — a finished lesson, a dictionary -/// drill, a mini-game — with an [icon] only where the design gives the kind -/// one: the lessons and the two drills. A game's kind is on its group's -/// heading, never repeated per row. +/// Flat on the page, not a card: its press highlight bleeds a stop past the +/// text (`margin: 0 -8px; padding: 12px 8px`), so the shelf sits that stop +/// inside the page gutter and every row pads it back. class ReplayRow extends StatelessWidget { /// Creates a [ReplayRow]. const ReplayRow({ @@ -89,6 +82,7 @@ class ReplayRow extends StatelessWidget { button: true, label: _announcement, hint: hint, + onTap: onTap, excludeSemantics: true, child: InkWell( onTap: onTap, diff --git a/lib/features/lessons/presentation/cards/bagpick_card_view.dart b/lib/features/lessons/presentation/cards/bagpick_card_view.dart index a84c2e96..c59bdbf2 100644 --- a/lib/features/lessons/presentation/cards/bagpick_card_view.dart +++ b/lib/features/lessons/presentation/cards/bagpick_card_view.dart @@ -300,6 +300,7 @@ class _CueRow extends StatelessWidget { label: isTell ? '${cue.label}. $body. This was the tell.' : '${cue.label}. $body', + onTap: onTap, excludeSemantics: true, child: Material( // A closed cue has to *look* closed, before its words are read: the diff --git a/lib/features/lessons/presentation/cards/card_cue_row.dart b/lib/features/lessons/presentation/cards/card_cue_row.dart index f4f4a83f..cec65842 100644 --- a/lib/features/lessons/presentation/cards/card_cue_row.dart +++ b/lib/features/lessons/presentation/cards/card_cue_row.dart @@ -68,13 +68,15 @@ class _HelpButton extends ConsumerWidget { final mood = context.mood; final help = ref.watch(cardKindHelpProvider(cue)).asData?.value; if (help == null) return const SizedBox.shrink(); + void openHelp() => showHelpDrawer(context, help); return Semantics( button: true, label: howToPlayLabel, + onTap: openHelp, excludeSemantics: true, child: InkResponse( - onTap: () => showHelpDrawer(context, help), + onTap: openHelp, radius: _target / 2, child: SizedBox( width: _target, diff --git a/lib/features/lessons/presentation/cards/card_option_tile.dart b/lib/features/lessons/presentation/cards/card_option_tile.dart index bbd4f64a..87342403 100644 --- a/lib/features/lessons/presentation/cards/card_option_tile.dart +++ b/lib/features/lessons/presentation/cards/card_option_tile.dart @@ -65,6 +65,7 @@ class CardOptionTile extends StatelessWidget { button: true, enabled: onTap != null, label: semanticsLabel, + onTap: onTap, excludeSemantics: true, child: OutlinedButton( onPressed: onTap, diff --git a/lib/features/lessons/presentation/cards/match_tile.dart b/lib/features/lessons/presentation/cards/match_tile.dart index a906347b..d1c09268 100644 --- a/lib/features/lessons/presentation/cards/match_tile.dart +++ b/lib/features/lessons/presentation/cards/match_tile.dart @@ -68,6 +68,7 @@ class MatchTile extends StatelessWidget { button: onTap != null, selected: state == MatchTileState.held, label: semanticsLabel, + onTap: onTap, excludeSemantics: true, child: Opacity( opacity: dragging ? OffTokens.matchDraggingOpacity.value : 1, diff --git a/lib/features/lessons/presentation/cards/pick_tile_row.dart b/lib/features/lessons/presentation/cards/pick_tile_row.dart index d037d15e..b84a8eee 100644 --- a/lib/features/lessons/presentation/cards/pick_tile_row.dart +++ b/lib/features/lessons/presentation/cards/pick_tile_row.dart @@ -86,6 +86,7 @@ class _PickTile extends StatelessWidget { button: true, selected: chosen, label: text, + onTap: onTap, excludeSemantics: true, child: OutlinedButton( onPressed: onTap, diff --git a/lib/features/onboarding/presentation/welcome/sound_toggle.dart b/lib/features/onboarding/presentation/welcome/sound_toggle.dart index ff58b446..ec424f61 100644 --- a/lib/features/onboarding/presentation/welcome/sound_toggle.dart +++ b/lib/features/onboarding/presentation/welcome/sound_toggle.dart @@ -10,18 +10,10 @@ const double _glyphSize = 19; /// Mute and unmute for the Welcome film. /// -/// A circle on the scrim rather than a chrome button: it floats over artwork, -/// so it takes the overlay palette both moods share and never the surface -/// tokens of the page beneath. -/// -/// Labelled by what a press *does*, not by what is true now — a reader hearing -/// "turn sound on" knows the outcome, where "muted" leaves them to work it out. -/// -/// **The scrim is taken whole here**, tint and blur. It is the one overlay -/// `OverlayBarrier` cannot render, because it is the only one of the four that -/// is not full-screen: its blur has to be shaped like the control it sits -/// behind, so the control clips it (#379). The design puts the film behind it -/// at 8px. +/// A circle on the scrim, labelled by what a press does rather than what is +/// true now. It takes the scrim whole, tint and blur — the one overlay +/// `OverlayBarrier` cannot render, since it is not full-screen and its blur +/// must be clipped to the control's shape (#379), blurring the film at 8px. class SoundToggle extends StatelessWidget { /// Creates a [SoundToggle]. const SoundToggle({required this.muted, required this.onPressed, super.key}); @@ -50,6 +42,7 @@ class SoundToggle extends StatelessWidget { return Semantics( button: true, label: muted ? 'Turn sound on' : 'Turn sound off', + onTap: onPressed, excludeSemantics: true, child: SizedBox( width: _targetSize, diff --git a/lib/features/path/presentation/path_lesson_row.dart b/lib/features/path/presentation/path_lesson_row.dart index 651dfaa9..a94a8b9e 100644 --- a/lib/features/path/presentation/path_lesson_row.dart +++ b/lib/features/path/presentation/path_lesson_row.dart @@ -99,10 +99,12 @@ class PathLessonRow extends StatelessWidget { /// meets the wall, and a dead row would say no without saying what it costs. Widget _row(BuildContext context, String title) { final locked = entry.isPurchaseLocked; + void openGate() => + unawaited(showPlusGate(context, LockedLesson(title: title))); final row = InkWell( onTap: locked - ? () => unawaited(showPlusGate(context, LockedLesson(title: title))) + ? openGate : () => unawaited(context.goToActivity(lessonRun(entry.lesson.id))), child: Opacity( opacity: locked ? _lockedOpacity : 1, @@ -128,6 +130,7 @@ class PathLessonRow extends StatelessWidget { return Semantics( button: true, label: LockedRowCopy.purchaseLockedSemantics(title), + onTap: openGate, excludeSemantics: true, child: row, ); diff --git a/lib/features/profile/presentation/widgets/appearance_selector.dart b/lib/features/profile/presentation/widgets/appearance_selector.dart index 9d3c4b20..343ea89f 100644 --- a/lib/features/profile/presentation/widgets/appearance_selector.dart +++ b/lib/features/profile/presentation/widgets/appearance_selector.dart @@ -10,15 +10,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// The `APPEARANCE` section's one row: a **Theme** label over three choices. /// -/// The design draws this as a row of its own rather than as a control dropped -/// on the screen: the label sits where every other row's label sits, the three -/// options fill the width beneath it, and the whole thing closes on the same -/// hairline. It was a bare Material `SegmentedButton` — no label, no rule, and -/// the options in the enum's order rather than the design's. -/// -/// Reads [ThemeModeController] rather than the settings row, because that -/// controller is the single source of truth for the appearance — the row backs -/// it, but nothing displays from the row. +/// Drawn as a row of its own, so the label sits where every other row's label +/// sits and it closes on the same hairline. Reads [ThemeModeController], the +/// single source of truth — the settings row backs it, nothing displays it. class AppearanceSelector extends ConsumerWidget { /// Creates an [AppearanceSelector]. const AppearanceSelector({super.key}); @@ -102,6 +96,7 @@ class _ThemeChoice extends StatelessWidget { button: true, selected: isSelected, label: mode.label, + onTap: onPick, excludeSemantics: true, child: Material( color: isSelected ? mood.accent : Colors.transparent, diff --git a/lib/features/profile/presentation/widgets/profile_card.dart b/lib/features/profile/presentation/widgets/profile_card.dart index 779e759c..31b72143 100644 --- a/lib/features/profile/presentation/widgets/profile_card.dart +++ b/lib/features/profile/presentation/widgets/profile_card.dart @@ -6,15 +6,8 @@ import 'package:flutter/material.dart'; /// and a tap that covers the whole card. /// /// The design makes each of these a `