77 * Four boxes on a row that is never wide enough for both controls — a phone
88 * answering "what do I search IN" then "what am I looking for". The two end
99 * caps are fixed: same place, same size, in both states, and either of them
10- * hands the floor to the other side. That is the whole reason the caps are
11- * OUTSIDE
12- * the controls rather than drawn inside them (a picker's façade yields a zone
13- * with `ownTarget`, a field has `Input.UI.LeftSlot`): an icon that lives
14- * inside its control while open and becomes a pill once closed is a switch
15- * that moves when you flip it, and the finger that opened the search has to
16- * travel to close it again. Out here, the same pixel opens and closes.
10+ * hands the floor to the other side. That is the whole reason the caps sit
11+ * OUTSIDE the controls rather than being drawn inside them (a picker's façade
12+ * yields a zone with `ownTarget`, a field has `Input.UI.LeftSlot`): an icon
13+ * that lives inside its control while open and becomes a pill once closed is
14+ * a switch that moves when you flip it: the finger that opened the search has
15+ * to travel to close it again. Out here, the same pixel does both.
1716 *
1817 * Nothing is unmounted. Two elements that never coexist have nothing to
1918 * interpolate between, so both sides stay mounted at all times and only their
2827 * mid-swap, and a collapsed control is not a zero-width column of stacked
2928 * words silently setting the row's height.
3029 *
31- * The square end cap cannot be had from CSS: on a flex item whose height comes
32- * from `align-self: stretch`, `aspect-ratio: 1` does not read that height (the
33- * main size is resolved from content first, measured the same in chromium,
34- * webkit and firefox). The row's height is whatever the controls in it happen
35- * to be, so a cap is measured and given its own height as a width.
30+ * The row has ONE height, and everything in it is that tall: the caps because
31+ * they are squares of it, the two controls because they are stretched to it.
32+ * Nothing is measured — a height read off the controls could only come back
33+ * through a resize observer, and a cap made square with `aspect-ratio` never
34+ * reads it anyway (on a flex item stretched to its line, the main size is
35+ * resolved from content first). It is a length, `--navi-control-swap-size`,
36+ * defaulting to the height of a navi control at its default padding; a row of
37+ * roomier controls says so once, there.
3638 */
3739
3840import { elementIsFocusable , findAfter } from "@jsenv/dom" ;
@@ -47,14 +49,25 @@ import { Button } from "../input/button.jsx";
4749
4850const css = /* css */ `
4951 .navi_control_swap {
52+ /* One navi control tall, and a caller with roomier controls than that
53+ overrides the length rather than every box that has to match it. */
54+ --x-control-swap-size: var(
55+ --navi-control-swap-size,
56+ calc(
57+ var(--navi-control-line-height) + 2 *
58+ var(--navi-control-padding-y-default) + 2 *
59+ var(--navi-control-border-width)
60+ )
61+ );
62+
63+ height: var(--x-control-swap-size);
5064 align-items: stretch;
5165
5266 > .navi_control_swap_cap {
5367 position: relative;
54- /* Its own height, measured (see the top comment). Unset until the first
55- measurement, where the cap is simply as wide as its icon. */
56- width: var(--navi-control-swap-cap-size);
57- flex: 0 0 auto;
68+ /* Square on the row: the same length gives the width, the height comes
69+ from the stretch. */
70+ flex: 0 0 var(--x-control-swap-size);
5871 align-items: center;
5972 align-self: stretch;
6073 justify-content: center;
@@ -96,6 +109,14 @@ const css = /* css */ `
96109 flex: 0 0 auto;
97110 flex-direction: column;
98111 justify-content: center;
112+
113+ /* A control is as wide as its content on its own (a navi one is
114+ literally width: fit-content), and here the one holding the floor
115+ has the whole middle to fill — so it is told to, rather than left
116+ to stretch, which an explicit width would win against anyway. */
117+ > * {
118+ width: 100%;
119+ }
99120 }
100121
101122 &[data-collapsed] {
@@ -153,6 +174,10 @@ const css = /* css */ `
153174 * @param animation - On by default: the two slots trade their share of the
154175 * middle over `--navi-control-swap-animation-duration` (0.22s). `false`
155176 * swaps them in one frame; `prefers-reduced-motion` does too.
177+ *
178+ * The row is one length tall — `--navi-control-swap-size`, the height of a navi
179+ * control at its default padding — and the caps are squares of it. Controls
180+ * with a padding of their own need that length said once, here.
156181 */
157182export const ControlSwap = ( props ) => {
158183 import . meta. css = css ;
@@ -246,8 +271,6 @@ export const ControlSwap = (props) => {
246271 } ) ;
247272 } , [ activeSide . name ] ) ;
248273
249- useCapSizeEffect ( capRefs ) ;
250-
251274 // Both caps do the same thing, and it is the reason they sit outside the
252275 // controls: whichever one the finger lands on, the floor goes to the other
253276 // side. The same pixel opens the search and closes it.
@@ -336,7 +359,9 @@ const ControlSwapCap = ({ ref, side, slotId, active, onPress }) => {
336359 aria-controls = { slotId }
337360 onClick = { onPress }
338361 >
339- < Icon fillLine > { icon } </ Icon >
362+ < Icon width = "60%" square >
363+ { icon }
364+ </ Icon >
340365 { badge ? (
341366 < span className = "navi_control_swap_badge" aria-hidden = "true" >
342367 { badge === true ? null : badge }
@@ -388,44 +413,3 @@ const findElementToFocus = (slot) => {
388413 }
389414 return findAfter ( slot , elementIsFocusable , { root : slot } ) ;
390415} ;
391-
392- // A cap is square on the row's height, and the row's height is whatever the
393- // controls in it happen to be — so it is watched rather than computed. Written
394- // on the cap itself: what it reads back is what it just measured, and a change
395- // too small to see is dropped so the observer cannot chase itself (the width it
396- // sets takes room away from the controls, which could in principle change the
397- // height it reads).
398- const CAP_SIZE_EPSILON = 0.5 ;
399- const useCapSizeEffect = ( capRefs ) => {
400- useLayoutEffect ( ( ) => {
401- const sizeWritten = new WeakMap ( ) ;
402- const syncCapSize = ( capElement ) => {
403- const { height } = capElement . getBoundingClientRect ( ) ;
404- const previousHeight = sizeWritten . get ( capElement ) ;
405- if (
406- previousHeight !== undefined &&
407- Math . abs ( previousHeight - height ) < CAP_SIZE_EPSILON
408- ) {
409- return ;
410- }
411- sizeWritten . set ( capElement , height ) ;
412- capElement . style . setProperty (
413- "--navi-control-swap-cap-size" ,
414- `${ height } px` ,
415- ) ;
416- } ;
417- const resizeObserver = new ResizeObserver ( ( entries ) => {
418- for ( const entry of entries ) {
419- syncCapSize ( entry . target ) ;
420- }
421- } ) ;
422- for ( const capRef of capRefs ) {
423- const capElement = capRef . current ;
424- syncCapSize ( capElement ) ;
425- resizeObserver . observe ( capElement ) ;
426- }
427- return ( ) => {
428- resizeObserver . disconnect ( ) ;
429- } ;
430- } , [ ] ) ;
431- } ;
0 commit comments