mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
app: the navbar corner wrapped onto a second row on small phones (#672)
* app: the navbar corner wrapped onto a second row on small phones Every status-icon slot carried a 1.5em separator margin -- ~80px across three slots -- which wrapped the mobile bar's whole corner onto a second row on any screen narrower than about 380px. The margin is now 0.25em on both bars: the corner's gap and the slots' own padding still space the icons, and on desktop the freed room just goes back to the links that useOverflowNav folds into "More". The mobile bar is extracted as MobileNav (mirroring DesktopNav) so a Cypress suite can mount it and hold it to one row at phone widths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: prove 320px with a faithful fixture, trim the CSS narration The suite claimed 360px covered narrower devices because the fixture was wider than production, but the fixture also lacked the real theme toggle's 0.8em right margin, so the safety margin was overstated. The fixture now matches a phone -- 32px logo home link, the toggle's margin, and a coarse pointer so the search link drops its keyboard hint, with a test asserting the hint is really absent -- and 320px is measured directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
42595400b4
commit
e66e1e9309
3 changed files with 131 additions and 10 deletions
|
|
@ -143,7 +143,9 @@ html .wrolpi-navbar .mantine-ActionIcon-root:hover {
|
|||
.wrolpi-navbar-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 1.5em;
|
||||
/* A sliver only: the corner's gap and the slots' own padding already separate the
|
||||
icons, and anything more has to fit a 320px phone times the number of slots. */
|
||||
margin-left: 0.25em;
|
||||
/* Full bar height, so the anchor inside has somewhere to stretch to. */
|
||||
align-self: stretch;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -439,14 +439,7 @@ export function NavBar() {
|
|||
|
||||
return <>
|
||||
<Media between={['mobile', 'tablet']}>
|
||||
<nav className='wrolpi-navbar' id='global_navbar' style={navBarStyle(navColors)}>
|
||||
{homeLink}
|
||||
<div className='wrolpi-navbar-right'>
|
||||
{icons}
|
||||
<SearchIconButton/>
|
||||
<MobileMenu links={[...allLinks, ...rightLinks]}/>
|
||||
</div>
|
||||
</nav>
|
||||
<MobileNav navColors={navColors} homeLink={homeLink} icons={icons}/>
|
||||
</Media>
|
||||
<Media greaterThanOrEqual='tablet'>
|
||||
<DesktopNav navColors={navColors} homeLink={homeLink} icons={icons}/>
|
||||
|
|
@ -454,6 +447,17 @@ export function NavBar() {
|
|||
</>
|
||||
}
|
||||
|
||||
export function MobileNav({navColors, homeLink, icons}) {
|
||||
return <nav className='wrolpi-navbar' id='global_navbar' style={navBarStyle(navColors)}>
|
||||
{homeLink}
|
||||
<div className='wrolpi-navbar-right'>
|
||||
{icons}
|
||||
<SearchIconButton/>
|
||||
<MobileMenu links={[...allLinks, ...rightLinks]}/>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
|
||||
export function DesktopNav({navColors, homeLink, icons}) {
|
||||
const containerRef = React.useRef(null);
|
||||
const homeRef = React.useRef(null);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {clearToasts, toast} from './toast';
|
|||
import {isDarkTheme, monochromeThemes, themeNames} from '../../themes/names';
|
||||
import {navColorNames} from '../../themes/navColors';
|
||||
import {NavBarSample} from '../ThemeSamplePage';
|
||||
import {DesktopNav, NavIconWrapper} from '../Nav';
|
||||
import {DesktopNav, MobileNav, NavIconWrapper} from '../Nav';
|
||||
import {APIButton} from '../Common';
|
||||
import {ShareButton} from '../Share';
|
||||
import {SortableTable} from '../SortableTable';
|
||||
|
|
@ -3734,6 +3734,121 @@ describe('the navigation bar never breaks onto a second line', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('the mobile navigation bar fits a phone on one row', () => {
|
||||
/*
|
||||
* The mobile bar showed the logo on one row and the whole corner -- share, hotspot,
|
||||
* theme, search, hamburger -- on a second, on any screen narrower than about 380px.
|
||||
* Every icon slot carried a 1.5em separator margin, which is ~80px across three
|
||||
* slots: more than the deficit. The slot margin is small now (`.wrolpi-navbar-icon`),
|
||||
* and this holds the bar to fitting a phone whatever the margin becomes.
|
||||
*/
|
||||
/*
|
||||
* The fixture matches what a phone actually renders, so the widths below are the widths
|
||||
* being proven -- not a stand-in with different contents:
|
||||
* - the home link is the 32px logo, the default (`NAME` is null in production);
|
||||
* - the theme slot carries the 0.8em right margin the real DarkModeToggle has;
|
||||
* - the pointer is made coarse (below), which is what makes a phone drop the search
|
||||
* link's keyboard hint.
|
||||
*/
|
||||
const indicators = <>
|
||||
<NavIconWrapper>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a href='#' data-testid='share'><Icon name='share' size='large'/></a>
|
||||
</NavIconWrapper>
|
||||
<NavIconWrapper>
|
||||
{/* No size inside icon={}: the icon-name audit in ui.test.js reads every string
|
||||
literal in such an expression as an icon name. */}
|
||||
<IconButton data-testid='hotspot' label='Hotspot' variant='subtle' icon={() =>
|
||||
<Icon name='wifi'/>}/>
|
||||
</NavIconWrapper>
|
||||
<NavIconWrapper>
|
||||
<IconButton data-testid='theme' icon='sun' label='Theme' variant='subtle'
|
||||
style={{marginRight: '0.8em'}}/>
|
||||
</NavIconWrapper>
|
||||
</>;
|
||||
|
||||
const homeLink = <a className='wrolpi-navbar-link' href='#'
|
||||
style={{paddingTop: 0, paddingBottom: 0}}>
|
||||
<i><img src='/icon.svg' height='32px' width='32px' alt='WROLPi Home Icon'/></i>
|
||||
</a>;
|
||||
|
||||
const mountAt = (width) => {
|
||||
/*
|
||||
* A phone has a coarse pointer. useKeyboardDetected keys off that and never turns
|
||||
* true without a keypress, so SearchIconButton drops its keyboard hint -- on the
|
||||
* desktop browser running this spec the hint renders and widens the corner by a
|
||||
* hint the phone never shows. Other queries pass through untouched; Mantine asks
|
||||
* its own. Guarded because a stub cannot wrap a method twice in one test.
|
||||
*/
|
||||
cy.window().then((win) => {
|
||||
if (win.matchMedia.restore) return;
|
||||
const real = win.matchMedia.bind(win);
|
||||
cy.stub(win, 'matchMedia').callsFake((query) => query === '(pointer: coarse)'
|
||||
? {
|
||||
matches: true, media: query, onchange: null,
|
||||
addEventListener: () => undefined, removeEventListener: () => undefined,
|
||||
addListener: () => undefined, removeListener: () => undefined,
|
||||
dispatchEvent: () => false,
|
||||
}
|
||||
: real(query));
|
||||
});
|
||||
return cy.viewport(width, 700).then(() => cy.mountUI(
|
||||
<MemoryRouter><div style={{width}}>
|
||||
<MobileNav
|
||||
navColors={{background: '#5c4fa8', color: '#ffffff', ratio: 6.69}}
|
||||
homeLink={homeLink}
|
||||
icons={indicators}
|
||||
/>
|
||||
</div></MemoryRouter>,
|
||||
));
|
||||
};
|
||||
|
||||
// Wide enough that nothing can wrap, so this is the height of exactly one row.
|
||||
let singleRow;
|
||||
|
||||
before(() => {
|
||||
mountAt(700);
|
||||
cy.get('.wrolpi-navbar').then(($nav) => {
|
||||
singleRow = $nav[0].getBoundingClientRect().height;
|
||||
expect(singleRow, 'a single row has a height to compare against')
|
||||
.to.be.greaterThan(30);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders what a phone renders: no keyboard hint on the search link', () => {
|
||||
// The teeth for the coarse-pointer stub. If the hint ever renders here, the widths
|
||||
// below are being proven against a wider bar than any phone shows.
|
||||
mountAt(375);
|
||||
cy.get('.wrolpi-navbar [aria-label="Search"]').should(($search) => {
|
||||
expect($search.text().trim(), 'search link shows a glyph only').to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
it('stays one row at small phone widths', () => {
|
||||
// 375px is the iPhone SE class that wrapped; 320px is the original iPhone SE.
|
||||
[414, 375, 320].forEach((width) => {
|
||||
mountAt(width);
|
||||
cy.get('.wrolpi-navbar').should(($nav) => {
|
||||
expect($nav[0].getBoundingClientRect().height, `bar height at ${width}px`)
|
||||
.to.be.at.most(singleRow + 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the whole corner inside the bar rather than clipping it', () => {
|
||||
// The failure a nowrap fix would trade the second row for.
|
||||
[375, 320].forEach((width) => {
|
||||
mountAt(width);
|
||||
cy.get('.wrolpi-navbar').should(($nav) => {
|
||||
const bar = $nav[0].getBoundingClientRect();
|
||||
const corner = $nav[0].querySelector('.wrolpi-navbar-right');
|
||||
expect(corner.getBoundingClientRect().right, `corner at ${width}px`)
|
||||
.to.be.at.most(bar.right + 0.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('the navbar overflow menu keeps the bar\'s styling', () => {
|
||||
/*
|
||||
* Mantine's Menu.Target clones its child and hands it a className of its own. The
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue