polish(ux): selected filter chips fill solid for clear contrast
The selected state used a pale tonal container (seedPrimaryContainer) that sat almost on top of the pale canvas, so 'on' barely read. Selected chips now fill solid — seedGreen for the plain attribute/market chips and the swatch's own ink for family/form chips — with white label, icon and checkmark. Idle chips stay light with dark ink, so the on/off jump is unmistakable. White-on-ink AA is locked by a new contrast test.
This commit is contained in:
parent
805f1dc535
commit
4a1e72bc7e
5 changed files with 53 additions and 12 deletions
|
|
@ -164,9 +164,9 @@ class _MonthStripState extends State<_MonthStrip> {
|
||||||
),
|
),
|
||||||
selected: selected,
|
selected: selected,
|
||||||
onSelected: (_) => widget.onSelect(m),
|
onSelected: (_) => widget.onSelect(m),
|
||||||
selectedColor: seedPrimaryContainer,
|
selectedColor: seedGreen,
|
||||||
labelStyle: TextStyle(
|
labelStyle: TextStyle(
|
||||||
color: selected ? seedOnPrimaryContainer : seedMuted,
|
color: selected ? Colors.white : seedMuted,
|
||||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ class _FilterBar extends StatelessWidget {
|
||||||
avatar: Icon(
|
avatar: Icon(
|
||||||
Icons.calendar_month,
|
Icons.calendar_month,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: state.sowThisMonthOnly ? null : seedGreen,
|
color: state.sowThisMonthOnly ? Colors.white : seedGreen,
|
||||||
),
|
),
|
||||||
label: Text(t.calendar.filterChip),
|
label: Text(t.calendar.filterChip),
|
||||||
selected: state.sowThisMonthOnly,
|
selected: state.sowThisMonthOnly,
|
||||||
|
|
@ -320,7 +320,7 @@ class _FilterBar extends StatelessWidget {
|
||||||
avatar: Icon(
|
avatar: Icon(
|
||||||
Icons.volunteer_activism_outlined,
|
Icons.volunteer_activism_outlined,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: state.sharingOnly ? null : seedGreen,
|
color: state.sharingOnly ? Colors.white : seedGreen,
|
||||||
),
|
),
|
||||||
label: Text(t.share.filterChip),
|
label: Text(t.share.filterChip),
|
||||||
selected: state.sharingOnly,
|
selected: state.sharingOnly,
|
||||||
|
|
@ -332,7 +332,7 @@ class _FilterBar extends StatelessWidget {
|
||||||
avatar: Icon(
|
avatar: Icon(
|
||||||
Icons.eco,
|
Icons.eco,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: state.organicOnly ? null : seedGreen,
|
color: state.organicOnly ? Colors.white : seedGreen,
|
||||||
),
|
),
|
||||||
label: Text(t.editVariety.organic),
|
label: Text(t.editVariety.organic),
|
||||||
selected: state.organicOnly,
|
selected: state.organicOnly,
|
||||||
|
|
@ -344,7 +344,7 @@ class _FilterBar extends StatelessWidget {
|
||||||
avatar: Icon(
|
avatar: Icon(
|
||||||
Icons.autorenew,
|
Icons.autorenew,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: state.needsReproductionOnly ? null : seedGreen,
|
color: state.needsReproductionOnly ? Colors.white : seedGreen,
|
||||||
),
|
),
|
||||||
label: Text(t.inventory.needsReproductionFilter),
|
label: Text(t.inventory.needsReproductionFilter),
|
||||||
selected: state.needsReproductionOnly,
|
selected: state.needsReproductionOnly,
|
||||||
|
|
@ -446,16 +446,23 @@ class _SwatchFilterChip extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Idle: a soft same-hue fill with dark ink. Selected: the ink becomes the
|
||||||
|
// fill and the content turns white, so the chip clearly reads as "on" while
|
||||||
|
// keeping its family/form hue. White on every ink meets WCAG AA — see
|
||||||
|
// test/ui/category_palette_contrast_test.dart.
|
||||||
|
final ink = selected ? Colors.white : swatch.ink;
|
||||||
return FilterChip(
|
return FilterChip(
|
||||||
label: Text(label),
|
label: Text(label),
|
||||||
avatar: icon == null ? null : Icon(icon, size: 18, color: swatch.ink),
|
avatar: icon == null ? null : Icon(icon, size: 18, color: ink),
|
||||||
selected: selected,
|
selected: selected,
|
||||||
onSelected: (_) => onSelected(),
|
onSelected: (_) => onSelected(),
|
||||||
backgroundColor: swatch.fill,
|
backgroundColor: swatch.fill,
|
||||||
selectedColor: swatch.ink.withValues(alpha: 0.24),
|
selectedColor: swatch.ink,
|
||||||
checkmarkColor: swatch.ink,
|
checkmarkColor: Colors.white,
|
||||||
side: BorderSide(color: swatch.ink.withValues(alpha: 0.35)),
|
side: BorderSide(
|
||||||
labelStyle: TextStyle(color: swatch.ink, fontWeight: FontWeight.w500),
|
color: swatch.ink.withValues(alpha: selected ? 0 : 0.35),
|
||||||
|
),
|
||||||
|
labelStyle: TextStyle(color: ink, fontWeight: FontWeight.w500),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -493,7 +493,7 @@ class _MarketFilterBar extends StatelessWidget {
|
||||||
avatar: Icon(
|
avatar: Icon(
|
||||||
Icons.eco,
|
Icons.eco,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: state.organicOnly ? null : seedGreen,
|
color: state.organicOnly ? Colors.white : seedGreen,
|
||||||
),
|
),
|
||||||
label: Text(t.editVariety.organic),
|
label: Text(t.editVariety.organic),
|
||||||
selected: state.organicOnly,
|
selected: state.organicOnly,
|
||||||
|
|
|
||||||
|
|
@ -105,5 +105,24 @@ ThemeData buildTaneTheme() {
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
color: seedDivider,
|
color: seedDivider,
|
||||||
),
|
),
|
||||||
|
// Filter/choice chips: idle chips stay light with dark ink, but a *selected*
|
||||||
|
// chip fills solid green with white ink so "on" reads unmistakably against
|
||||||
|
// the pale canvas (the soft tonal default sat too close to it). White on
|
||||||
|
// seedGreen meets WCAG AA — see test/ui/theme_contrast_test.dart. Chips that
|
||||||
|
// carry their own hue (family/form swatches) override this per-swatch.
|
||||||
|
chipTheme: const ChipThemeData(
|
||||||
|
selectedColor: seedGreen,
|
||||||
|
checkmarkColor: Colors.white,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: seedOnSurface,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
secondaryLabelStyle: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,19 @@ void main() {
|
||||||
reason: '$type ink on fill');
|
reason: '$type ink on fill');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A *selected* swatch chip fills solid with its ink and draws white content
|
||||||
|
// (label, icon, checkmark). That pairing must clear AA too.
|
||||||
|
test('white content meets AA on every selected (ink-filled) swatch', () {
|
||||||
|
for (var i = 0; i < 60; i++) {
|
||||||
|
final s = categorySwatch('family-$i');
|
||||||
|
expect(contrast(Colors.white, s.ink), greaterThanOrEqualTo(4.5),
|
||||||
|
reason: 'white on category-$i ink');
|
||||||
|
}
|
||||||
|
for (final type in LotType.values) {
|
||||||
|
final s = lotTypeSwatch(type);
|
||||||
|
expect(contrast(Colors.white, s.ink), greaterThanOrEqualTo(4.5),
|
||||||
|
reason: 'white on $type ink');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue