mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
All 43 cypress specs pass now (was 36 of 43). Five of the seven failures were
stale selectors, two were real.
Real, and both mine:
The search modal stopped focusing its input. Mantine mounts modal content on
a later tick, so the effect that focused it ran while the ref was still null;
Semantic's modal mounted synchronously, which is why focusing there had been
enough. Fixed declaratively -- the field asks for focus itself, and SearchBox
also sets `data-autofocus`, which is what Mantine's focus trap honours when it
takes focus away from a plain autoFocus.
Escape closed the suggestion list and the enclosing modal at once, so a user
correcting a mistyped query lost the whole modal. Escape now dismisses the
suggestions and stops there; a second Escape, with the list closed, still
closes the modal.
Stale selectors, all in the specs: Semantic's icon font (`i.search.icon`) is
SVG now, its `.ui.modal`/`.ui.dimmer` are the library's modal and overlay, and
one test asserted a button carried the classes `ui button secondary`. That last
one is rewritten to check what a user can actually perceive -- that Edit is a
real link pointing at the edit route -- because framework class names say
nothing and break on every library change.
Two accessibility gaps the tests walked into:
The nav's search control was an anchor with an onClick, no name and no
keyboard path. It now has a name, a role and Enter/Space activation, which is
also what let the spec stop matching on icon-font classes.
The search dialog had no accessible name at all; it has one now.
One `{force: true}` remains, with its reason recorded: Cypress judges that
field covered because it sits in a fixed modal under a sticky header, and I
verified by hand at Cypress's own viewport that it is the topmost element at
its centre, unobscured, and focused on open.
Also closed a hole in the ratchet that this uncovered. A file could drop its
`semantic-ui-react` imports, leave the baseline, and still render Semantic
through Theme.tsx's wrappers -- which is exactly how a Semantic modal survived
in the search shortcut long after that file looked migrated. A test now
forbids it, and the three files doing it (Calculators, KeyboardShortcutsProvider,
and Common.js reaching for theme names) import from the library or from
themes/names instead.
96 lines
4.9 KiB
JavaScript
96 lines
4.9 KiB
JavaScript
import React from "react";
|
|
import {Breadcrumbs, useTitle} from "./Common";
|
|
import {Button, Header, Icon} from "./ui";
|
|
import {useCalcQuery} from "../hooks/customHooks";
|
|
import {TemperatureCalculator} from "./calculators/TemperatureCalculator";
|
|
import {ElectricalCalculators} from "./calculators/ElectricalCalculators";
|
|
import {DipoleAntennaCalculator} from "./calculators/HamCalculators";
|
|
import {Link} from "react-router";
|
|
import {RatioCalculators} from "./calculators/RatioCalculator";
|
|
import {QRCodeCalculator} from "./calculators/QRCodeCalculator";
|
|
import {DriveCalculator} from "./calculators/DriveCalculator";
|
|
import {WaterCalculator} from "./calculators/WaterCalculator";
|
|
import {FoodStorageCalculator} from "./calculators/FoodStorageCalculator";
|
|
import {OneTimePadCalculator} from "./calculators/OneTimePadCalculator";
|
|
import {VinDecoderCalculator} from "./calculators/VinDecoderCalculator";
|
|
|
|
// Calculators are shown in these titled groups, in this order. Each calculator carries a
|
|
// `group` matching one of these names; any calculator whose group is missing/unknown falls
|
|
// into a trailing "Other" section so a new calculator can never silently disappear.
|
|
//
|
|
// `color` is a Semantic UI named color (colorblind-friendly palette) used to tint the group's
|
|
// buttons. Violet is intentionally avoided — it is the default navbar color and would blend in.
|
|
export const CALCULATOR_GROUPS = [
|
|
{name: 'Preparedness & Storage', color: 'red'},
|
|
{name: 'Radio & Communications', color: 'blue'},
|
|
{name: 'Engineering', color: 'yellow'},
|
|
{name: 'Tools & Reference', color: 'grey'},
|
|
];
|
|
|
|
export const useCalculators = () => {
|
|
const [calc, setCalc] = useCalcQuery();
|
|
|
|
const calculators = [
|
|
{key: 'ratio', icon: 'th large', button: 'Ratio', group: 'Engineering', contents: <RatioCalculators/>},
|
|
{key: 'electrical', icon: 'lightning', button: 'Electrical', group: 'Engineering', contents: <ElectricalCalculators/>},
|
|
{key: 'drive', icon: 'cogs', button: 'Drive Ratio', group: 'Engineering', contents: <DriveCalculator/>},
|
|
{key: 'water', icon: 'tint', button: 'Water Storage', group: 'Preparedness & Storage', contents: <WaterCalculator/>},
|
|
{key: 'ration', icon: 'food', button: 'Food Storage', group: 'Preparedness & Storage', contents: <FoodStorageCalculator/>},
|
|
{key: 'antenna', icon: 'signal', button: 'Antenna', group: 'Radio & Communications', contents: <DipoleAntennaCalculator/>},
|
|
{key: 'temperature', icon: 'thermometer', button: 'Temperature', group: 'Tools & Reference', contents: <TemperatureCalculator/>},
|
|
{key: 'qrCode', icon: 'qrcode', button: 'QR Code', group: 'Tools & Reference', contents: <QRCodeCalculator/>},
|
|
{key: 'otp', icon: 'lock', button: 'One Time Pad', title: 'One Time Pad', group: 'Radio & Communications', contents: <OneTimePadCalculator/>},
|
|
{key: 'vin', icon: 'car', button: 'VIN Decoder', title: 'VIN Decoder', group: 'Tools & Reference', contents: <VinDecoderCalculator/>},
|
|
];
|
|
|
|
const activeCalculator = calc ? calculators.find(i => i.key === calc) : null;
|
|
|
|
const calculatorButton = ({button, icon, key}, color) =>
|
|
<Link key={key} to={`/more/calculators?calc=${key}`}>
|
|
<Button color={color} style={{margin: '0.5em'}}>
|
|
{icon && <Icon name={icon}/>}
|
|
{button}
|
|
</Button>
|
|
</Link>;
|
|
|
|
// Render each defined group as a titled section, then any leftovers under "Other".
|
|
const groupNames = CALCULATOR_GROUPS.map(g => g.name);
|
|
const grouped = CALCULATOR_GROUPS.map(g => [g.name, g.color, calculators.filter(c => c.group === g.name)]);
|
|
const leftovers = calculators.filter(c => !groupNames.includes(c.group));
|
|
if (leftovers.length > 0) {
|
|
grouped.push(['Other', undefined, leftovers]);
|
|
}
|
|
|
|
const calculatorLinks = grouped
|
|
.filter(([, , items]) => items.length > 0)
|
|
.map(([name, color, items]) =>
|
|
<div key={name} style={{marginBottom: '1.5em'}}>
|
|
<Header as='h3'>{name}</Header>
|
|
{items.map(c => calculatorButton(c, color))}
|
|
</div>);
|
|
|
|
return {calc, calculators, activeCalculator, calculatorLinks}
|
|
}
|
|
|
|
export function CalculatorsPage() {
|
|
const {activeCalculator, calculatorLinks} = useCalculators();
|
|
|
|
const {icon, button, title, contents} = activeCalculator || {};
|
|
const name = activeCalculator ? title || button + ' Calculator' : 'Calculators';
|
|
useTitle(name);
|
|
|
|
const body = activeCalculator ? <span>{contents}</span> : calculatorLinks;
|
|
|
|
// Make first breadcrumb a link back to Calculators only when a calculator is active.
|
|
const crumbs = activeCalculator
|
|
? [{text: 'Calculators', link: '/more/calculators'}, {text: name, icon: icon}]
|
|
: [{text: 'Calculators'}];
|
|
|
|
return <>
|
|
<Breadcrumbs crumbs={crumbs} size='huge'/>
|
|
|
|
<div style={{marginTop: '1em'}}>
|
|
{body}
|
|
</div>
|
|
</>
|
|
}
|