Merge branch 'op-panel-layout' into selection-follows-sort
This commit is contained in:
commit
c0d58e1000
@ -98,39 +98,72 @@ const LABEL_W = 'w-24'
|
|||||||
// toggle is skipped, while still costing the dock a row. Hovering gives it
|
// toggle is skipped, while still costing the dock a row. Hovering gives it
|
||||||
// (native title, which nothing can clip), clicking pins it.
|
// (native title, which nothing can clip), clicking pins it.
|
||||||
//
|
//
|
||||||
// It opens upward, because the settings it explains sit low in a dock that
|
// Positioned `fixed` against the icon's own rect rather than absolutely inside
|
||||||
// scrolls -- a popover hanging below them is cut off by the dock's own edge.
|
// it. Absolute meant the dock clipped it: the panel is an overflow-auto box, so
|
||||||
// `dir="down"` is for the few that sit at the top of the panel, where up is the
|
// a popover under a setting at the bottom was cut off by the dock's edge and
|
||||||
// clipped direction instead.
|
// one above a control at the top was cut off by its top. Direction was a prop
|
||||||
function Help({ text, dir = 'up' }) {
|
// for a while, which only moved the clipping around -- fixed leaves the box
|
||||||
const [open, setOpen] = useState(false)
|
// altogether, so it can prefer up everywhere and flip down only when the window
|
||||||
|
// itself has no room.
|
||||||
|
const HELP_W = 256 // px; matches the rendered width
|
||||||
|
function Help({ text }) {
|
||||||
|
const [at, setAt] = useState(null)
|
||||||
|
const btnRef = useRef(null)
|
||||||
|
|
||||||
|
const place = () => {
|
||||||
|
const r = btnRef.current?.getBoundingClientRect()
|
||||||
|
if (!r) return
|
||||||
|
const left = Math.max(8, Math.min(r.left, window.innerWidth - HELP_W - 8))
|
||||||
|
// Up unless the top of the window is nearer than the box is tall.
|
||||||
|
const up = r.top > 160
|
||||||
|
setAt(up
|
||||||
|
? { left, bottom: window.innerHeight - r.top + 6 }
|
||||||
|
: { left, top: r.bottom + 6 })
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!at) return
|
||||||
|
const close = () => setAt(null)
|
||||||
|
// Any scroll moves the anchor out from under a fixed box, so it closes
|
||||||
|
// rather than floating somewhere it no longer points at.
|
||||||
|
window.addEventListener('scroll', close, true)
|
||||||
|
window.addEventListener('resize', close)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', close, true)
|
||||||
|
window.removeEventListener('resize', close)
|
||||||
|
}
|
||||||
|
}, [at])
|
||||||
|
|
||||||
if (!text) return null
|
if (!text) return null
|
||||||
return (
|
return (
|
||||||
<span className="relative inline-flex shrink-0">
|
<>
|
||||||
<button type="button" title={text} onClick={() => setOpen(o => !o)} onBlur={() => setOpen(false)}
|
<button ref={btnRef} type="button" title={text}
|
||||||
|
onClick={() => (at ? setAt(null) : place())}
|
||||||
|
onBlur={() => setAt(null)}
|
||||||
aria-label={text}
|
aria-label={text}
|
||||||
className={`w-4 h-4 rounded-full border leading-none flex items-center justify-center transition-colors ${
|
className={`w-4 h-4 rounded-full border leading-none flex items-center justify-center shrink-0 transition-colors ${
|
||||||
open ? 'border-blue-400 text-blue-600 bg-blue-50' : 'border-gray-300 text-gray-500 hover:text-blue-600 hover:border-blue-400'}`}
|
at ? 'border-blue-400 text-blue-600 bg-blue-50' : 'border-gray-300 text-gray-500 hover:text-blue-600 hover:border-blue-400'}`}
|
||||||
style={{ fontSize: '10px' }}>?</button>
|
style={{ fontSize: '10px' }}>?</button>
|
||||||
{open && (
|
{at && (
|
||||||
<span className={`absolute z-20 left-0 w-64 rounded border border-gray-200 bg-white shadow-lg px-2 py-1.5
|
<span
|
||||||
text-gray-600 leading-snug normal-case tracking-normal
|
className="fixed z-50 rounded border border-gray-200 bg-white shadow-lg px-2 py-1.5
|
||||||
${dir === 'down' ? 'top-5' : 'bottom-5'}`}>
|
text-gray-600 leading-snug normal-case tracking-normal"
|
||||||
|
style={{ ...at, width: HELP_W }}>
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// `stack` puts the label above its control instead of beside it. The aligned
|
// `stack` puts the label above its control instead of beside it. The aligned
|
||||||
// label column costs 6rem, which is most of a narrow zone -- the tag field in
|
// label column costs 6rem, which is most of a narrow zone -- the tag field in
|
||||||
// the bottom dock had 12rem of box left to type an initiative name into.
|
// the bottom dock had 12rem of box left to type an initiative name into.
|
||||||
function Field({ label, children, hint, hintDir, stack }) {
|
function Field({ label, children, hint, stack }) {
|
||||||
if (stack) {
|
if (stack) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<span className="text-gray-600 whitespace-nowrap flex items-center gap-1.5">{label}<Help text={hint} dir={hintDir} /></span>
|
<span className="text-gray-600 whitespace-nowrap flex items-center gap-1.5">{label}<Help text={hint} /></span>
|
||||||
<div className="flex items-center gap-2 min-w-0">{children}</div>
|
<div className="flex items-center gap-2 min-w-0">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -140,7 +173,7 @@ function Field({ label, children, hint, hintDir, stack }) {
|
|||||||
<span className={`text-gray-600 whitespace-nowrap shrink-0 pt-1 ${LABEL_W}`}>{label}</span>
|
<span className={`text-gray-600 whitespace-nowrap shrink-0 pt-1 ${LABEL_W}`}>{label}</span>
|
||||||
<div className="flex items-center gap-2 flex-wrap min-w-0">
|
<div className="flex items-center gap-2 flex-wrap min-w-0">
|
||||||
{children}
|
{children}
|
||||||
<Help text={hint} dir={hintDir} />
|
<Help text={hint} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -1540,7 +1573,7 @@ export default function OperationPanel({
|
|||||||
with no heading over it, so the panel never said what it was about
|
with no heading over it, so the panel never said what it was about
|
||||||
to do until you read the button. */}
|
to do until you read the button. */}
|
||||||
<div className="flex items-start gap-4 flex-wrap px-3 py-2 border-b border-gray-200 bg-gray-50/60">
|
<div className="flex items-start gap-4 flex-wrap px-3 py-2 border-b border-gray-200 bg-gray-50/60">
|
||||||
<Field label="Change" hintDir="down" hint={OP_HELP[activeOp]}>
|
<Field label="Change" hint={OP_HELP[activeOp]}>
|
||||||
<div className="inline-flex rounded border border-gray-300 divide-x divide-gray-300 shadow-sm overflow-hidden">
|
<div className="inline-flex rounded border border-gray-300 divide-x divide-gray-300 shadow-sm overflow-hidden">
|
||||||
{['scale', 'recode', 'clone'].map(op => (
|
{['scale', 'recode', 'clone'].map(op => (
|
||||||
<Button key={op} onClick={() => setActiveOp(op)} active={activeOp === op}>
|
<Button key={op} onClick={() => setActiveOp(op)} active={activeOp === op}>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user