Tab layout: the round ☰ is a small fixed corner button on phones
The ☰ no longer rides above the legend sheet: that lift made it fly on scroll and re-animate on a tab switch. It is now 40px, 8px from the bottom-start corner of the layout (plus the safe area; bottom-right in Arabic), and never moves: no lift, no scroll tracking, no transition. It sits over the corner of the legend sheet; the collapsed sheet pads its start edge by --map-fab-beside (now 56px = 8 + 40 + 8) so its caret and title clear it. --map-fab-clearance stays 56px. The legend-sheet listener, its measurement and --map-fab-sheet-inset are removed (the sheet may keep dispatching its event). The open drawer keeps z 100000. Both CSS copies change identically.
This commit is contained in:
parent
59fd232a1f
commit
4efe3744da
1 changed files with 14 additions and 92 deletions
|
|
@ -103,8 +103,8 @@
|
|||
sit NEXT TO the circle rather than on top of it (12px inset + 44px button + 8px gap). A
|
||||
consumer that hardcoded 64px would silently overlap the day the circle resizes; reading it
|
||||
means the circle's geometry is stated once, here, by the component that owns it. -->
|
||||
<div ref="vtlRoot" class="vtl" :class="{ 'nav-open': navOpen, 'rail-collapsed': railCollapsed, 'no-header': !header }"
|
||||
:style="fabShown ? { '--map-fab-clearance': 'calc(56px + env(safe-area-inset-bottom) + ' + fabLift + 'px)', '--map-fab-beside': 'calc(64px + env(safe-area-inset-left))', '--vtl-fab-lift': fabLift + 'px', ...(fabLift > 0 ? { '--map-fab-sheet-inset': '0px' } : {}) } : null">
|
||||
<div class="vtl" :class="{ 'nav-open': navOpen, 'rail-collapsed': railCollapsed, 'no-header': !header }"
|
||||
:style="fabShown ? { '--map-fab-clearance': 'calc(56px + env(safe-area-inset-bottom))', '--map-fab-beside': 'calc(56px + env(safe-area-inset-left))' } : null">
|
||||
<header class="vtl-top" v-if="header">
|
||||
<button class="vtl-menu" type="button" :aria-expanded="String(navOpen)" @click="navOpen = !navOpen"
|
||||
:aria-label="label('toggleList')">☰</button>
|
||||
|
|
@ -332,7 +332,10 @@ const VTL_SHADOW_CSS = `
|
|||
content but BELOW the drawer (.vtl-nav is z-index:20) so the drawer covers it when it slides in.
|
||||
44px is the minimum comfortable touch target; the safe-area insets keep it clear of an iOS home
|
||||
indicator and a notch in landscape. */
|
||||
.vtl-fab { position:absolute; z-index:15; left:calc(12px + env(safe-area-inset-left)); bottom:calc(12px + env(safe-area-inset-bottom)); transform:translateY(calc(-1 * var(--vtl-fab-lift, 0px))); transition:transform .2s ease; width:44px; height:44px; display:flex; align-items:center; justify-content:center; font:18px/1 system-ui; color:#3b463d; background:#ffffff; border:1px solid var(--vtl-border); border-radius:50%; box-shadow:0 2px 10px rgba(0,0,0,.18); cursor:pointer; }
|
||||
/* THE CORNER CIRCLE NEVER MOVES: 40px, 8px from the bottom-start corner of the layout (plus the safe area). It does not
|
||||
ride above a legend sheet or follow a scroll; it sits over the sheet's corner, and the collapsed sheet pads its
|
||||
start edge by --map-fab-beside so its caret and title clear it. A fixed spot cannot fly or re-animate. */
|
||||
.vtl-fab { position:absolute; z-index:15; left:calc(8px + env(safe-area-inset-left)); bottom:calc(8px + env(safe-area-inset-bottom)); width:40px; height:40px; display:flex; align-items:center; justify-content:center; font:18px/1 system-ui; color:#3b463d; background:#ffffff; border:1px solid var(--vtl-border); border-radius:50%; box-shadow:0 2px 10px rgba(0,0,0,.18); cursor:pointer; }
|
||||
.vtl-fab:hover { background:#d1d4cd; }
|
||||
.vtl-fab:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:2px; }
|
||||
.vtl-menu:hover { background:#d1d4cd; }
|
||||
|
|
@ -553,7 +556,7 @@ const VTL_SHADOW_CSS = `
|
|||
.vtl:dir(rtl) .vtl-meta, .vtl:dir(rtl) .vtl-td, .vtl:dir(rtl) .vtl-chev { margin-left:0; margin-right:auto; }
|
||||
.vtl:dir(rtl) .vtl-chev:not(.open) { transform:scaleX(-1); }
|
||||
.vtl:dir(rtl) .vtl-collapse-ic svg { transform:scaleX(-1); }
|
||||
.vtl:dir(rtl) .vtl-fab { left:auto; right:calc(12px + env(safe-area-inset-right)); }
|
||||
.vtl:dir(rtl) .vtl-fab { left:auto; right:calc(8px + env(safe-area-inset-right)); }
|
||||
.vtl.rail-collapsed:dir(rtl) .vtl-nav { transform:translateX(184px); }
|
||||
.vtl.rail-collapsed:dir(rtl) .vtl-main { left:0; right:72px; }
|
||||
.vtl.rail-collapsed:dir(rtl) .vtl-collapse { padding-left:10px; padding-right:184px; }
|
||||
|
|
@ -624,9 +627,7 @@ export default defineComponent({
|
|||
// railCollapsed = desktop slim rail) at the breakpoint and drive rail visibility correctly.
|
||||
narrow: false, _mql: null, _onMedia: null,
|
||||
activatedKeys: {}, hostState: {}, hostError: {}, _hostSlots: {}, _hostMounted: {},
|
||||
hostEls: {},
|
||||
// sheetLift: px the circle rises to clear the VISIBLE tab's phone legend sheet (0 = none, today's place).
|
||||
sheetLift: 0
|
||||
hostEls: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -639,16 +640,6 @@ export default defineComponent({
|
|||
* test for it.
|
||||
*/
|
||||
fabShown() { return this.narrow && !this.header && !this.navOpen },
|
||||
// THE CIRCLE RIDES ABOVE A PHONE LEGEND SHEET. A map's LegendMobile announces its sheet with the composed
|
||||
// 'dxmaps:legend-sheet' event, dispatched FROM the sheet element; the layout measures that element itself,
|
||||
// so the circle sits 8px above the sheet's TOP wherever the map ends (Simple Research's map stops 102px above
|
||||
// the viewport, with the grid below) and in both states, collapsed or open: the two never overlap. The
|
||||
// measurement is redone on the pane's scroll and on resize. No visible sheet (another tab's, a scrolled-away
|
||||
// map, a map without one, an older build): 0, exactly today's placement.
|
||||
// While the circle rides above a sheet, that sheet may take the full width: --map-fab-sheet-inset:0 tells it so
|
||||
// (LegendMobile's start inset is var(--map-fab-sheet-inset, var(--map-fab-beside))). Unset otherwise, so a
|
||||
// sheet the circle is NOT lifted over keeps its room beside it.
|
||||
fabLift() { return this.sheetLift },
|
||||
/**
|
||||
* The element the back-to-top arrow scrolls. .vtl-host is the shell's scroller
|
||||
* (overflow-y:auto) and there is one PER TAB, so this follows `active` rather than binding a
|
||||
|
|
@ -687,7 +678,7 @@ export default defineComponent({
|
|||
// then keep alive). Non-element tabs are ignored here (the host renders those in #default).
|
||||
active: {
|
||||
immediate: true,
|
||||
handler(key) { this.activateElement(key); this.$nextTick(() => this.recomputeLift()) }
|
||||
handler(key) { this.activateElement(key) }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -709,15 +700,6 @@ export default defineComponent({
|
|||
// The immediate `active` watcher may have run before the pane div existed; re-run for the current
|
||||
// active key now that the DOM is up, so an element-tab active at first render mounts.
|
||||
this.$nextTick(() => this.activateElement(this.active))
|
||||
this._onSheet = (e) => this.onLegendSheet(e)
|
||||
// On the root ELEMENT by ref: $el is the first node of the template, which is not the root div when the
|
||||
// template starts with a comment (dev builds keep comments).
|
||||
this.$refs.vtlRoot?.addEventListener('dxmaps:legend-sheet', this._onSheet)
|
||||
// The pane scrolls the map (and its sheet) under the circle: a capture-phase listener sees the scroll of
|
||||
// any scroller in this tree. Resize moves the map's bottom too.
|
||||
this._onLiftMove = () => { if (this._sheets && this._sheets.size) this.scheduleLift() }
|
||||
this.$refs.vtlRoot?.addEventListener('scroll', this._onLiftMove, { capture: true, passive: true })
|
||||
window.addEventListener('resize', this._onLiftMove, { passive: true })
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
|
|
@ -727,72 +709,9 @@ export default defineComponent({
|
|||
}
|
||||
} catch { /* ignore */ }
|
||||
this._mql = null; this._onMedia = null
|
||||
if (this._onSheet) this.$refs.vtlRoot?.removeEventListener('dxmaps:legend-sheet', this._onSheet)
|
||||
if (this._onLiftMove) { this.$refs.vtlRoot?.removeEventListener('scroll', this._onLiftMove, { capture: true }); window.removeEventListener('resize', this._onLiftMove) }
|
||||
if (this._liftRaf) cancelAnimationFrame(this._liftRaf)
|
||||
if (this._liftTimer) clearTimeout(this._liftTimer)
|
||||
this._sheetRO?.disconnect(); this._sheetRO = null
|
||||
this._onSheet = null; this._onLiftMove = null; this._sheets = null
|
||||
},
|
||||
methods: {
|
||||
registerTab(key, el) { if (el) this._tabEls[key] = el },
|
||||
// One entry per announcing sheet (its own element, from the composed path), so several mounted tabs can
|
||||
// each announce; only the one on screen moves the circle. state 'gone' (unmount, desktop width) drops it.
|
||||
onLegendSheet(e) {
|
||||
const origin = (e.composedPath && e.composedPath()[0]) || e.target
|
||||
const d = (e && e.detail) || {}
|
||||
if (!this._sheets) this._sheets = new Set()
|
||||
// The layout also watches each announced sheet's own size: a ResizeObserver fires after layout, so an
|
||||
// open/close or a drag re-measures from the box as drawn, whatever the timing of the event itself.
|
||||
if (!this._sheetRO && typeof ResizeObserver === 'function') this._sheetRO = new ResizeObserver(() => this.scheduleLift())
|
||||
if (d.state === 'gone') { this._sheets.delete(origin); this._sheetRO?.unobserve(origin) }
|
||||
else if (!this._sheets.has(origin)) { this._sheets.add(origin); this._sheetRO?.observe(origin) }
|
||||
// Measure now AND after the next frame: a sheet may announce a state change before its own DOM update and
|
||||
// layout have happened, so a synchronous read alone can see the previous box.
|
||||
this.recomputeLift()
|
||||
this.scheduleLift()
|
||||
},
|
||||
// Coalesced to one measurement per frame (scroll and resize fire far more often than that). A 100ms timer
|
||||
// backs the frame up: a page that is not the visible tab gets no animation frames at all.
|
||||
scheduleLift() {
|
||||
if (this._liftRaf || this._liftTimer) return
|
||||
const run = () => {
|
||||
if (this._liftRaf) cancelAnimationFrame(this._liftRaf)
|
||||
if (this._liftTimer) clearTimeout(this._liftTimer)
|
||||
this._liftRaf = 0; this._liftTimer = 0
|
||||
this.recomputeLift()
|
||||
}
|
||||
this._liftRaf = requestAnimationFrame(run)
|
||||
this._liftTimer = setTimeout(run, 100)
|
||||
},
|
||||
recomputeLift() {
|
||||
const root = this.$refs.vtlRoot
|
||||
let lift = 0
|
||||
if (root && this._sheets && this._sheets.size) {
|
||||
const box = root.getBoundingClientRect()
|
||||
// The circle's RESTING bottom offset (12px + the safe area, as its CSS resolves it): the lift is measured
|
||||
// from where the circle already sits, so the gap to the sheet is exactly 8px.
|
||||
const fab = root.querySelector('.vtl-fab')
|
||||
const rest = fab ? (parseFloat(getComputedStyle(fab).bottom) || 0) : 12
|
||||
for (const el of this._sheets) {
|
||||
if (!el || !el.isConnected) { this._sheets.delete(el); this._sheetRO?.unobserve(el); continue }
|
||||
if (!this.isShown(el)) continue
|
||||
const q = el.getBoundingClientRect()
|
||||
// on screen inside the layout's box: its top edge is what the circle must clear
|
||||
if (q.height > 0 && q.top < box.bottom && q.bottom > box.top) lift = Math.max(lift, Math.round(box.bottom - rest - q.top + 8))
|
||||
}
|
||||
}
|
||||
this.sheetLift = lift
|
||||
},
|
||||
// On screen = rendered and not inside a hidden, inert or display:none pane (the gallery hides inactive
|
||||
// panes with v-show + hidden; another host may use inert), walking out through shadow roots.
|
||||
isShown(el) {
|
||||
if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false
|
||||
for (let n = el; n; n = n.parentNode || n.host) {
|
||||
if (n.nodeType === 1 && (n.hasAttribute('hidden') || n.hasAttribute('inert'))) return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
// The rail toggle. On a narrow screen the rail is an off-canvas drawer, so the toggle closes the
|
||||
// drawer (navOpen). On desktop it collapses/expands the slim icon rail (railCollapsed, persisted
|
||||
// in component data). Reading matchMedia at click time keeps one toggle correct in both modes.
|
||||
|
|
@ -991,7 +910,10 @@ export default defineComponent({
|
|||
content but BELOW the drawer (.vtl-nav is z-index:20) so the drawer covers it when it slides in.
|
||||
44px is the minimum comfortable touch target; the safe-area insets keep it clear of an iOS home
|
||||
indicator and a notch in landscape. */
|
||||
.vtl-fab { position:absolute; z-index:15; left:calc(12px + env(safe-area-inset-left)); bottom:calc(12px + env(safe-area-inset-bottom)); transform:translateY(calc(-1 * var(--vtl-fab-lift, 0px))); transition:transform .2s ease; width:44px; height:44px; display:flex; align-items:center; justify-content:center; font:18px/1 system-ui; color:#3b463d; background:#ffffff; border:1px solid var(--vtl-border); border-radius:50%; box-shadow:0 2px 10px rgba(0,0,0,.18); cursor:pointer; }
|
||||
/* THE CORNER CIRCLE NEVER MOVES: 40px, 8px from the bottom-start corner of the layout (plus the safe area). It does not
|
||||
ride above a legend sheet or follow a scroll; it sits over the sheet's corner, and the collapsed sheet pads its
|
||||
start edge by --map-fab-beside so its caret and title clear it. A fixed spot cannot fly or re-animate. */
|
||||
.vtl-fab { position:absolute; z-index:15; left:calc(8px + env(safe-area-inset-left)); bottom:calc(8px + env(safe-area-inset-bottom)); width:40px; height:40px; display:flex; align-items:center; justify-content:center; font:18px/1 system-ui; color:#3b463d; background:#ffffff; border:1px solid var(--vtl-border); border-radius:50%; box-shadow:0 2px 10px rgba(0,0,0,.18); cursor:pointer; }
|
||||
.vtl-fab:hover { background:#d1d4cd; }
|
||||
.vtl-fab:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:2px; }
|
||||
.vtl-menu:hover { background:#d1d4cd; }
|
||||
|
|
@ -1212,7 +1134,7 @@ export default defineComponent({
|
|||
.vtl:dir(rtl) .vtl-meta, .vtl:dir(rtl) .vtl-td, .vtl:dir(rtl) .vtl-chev { margin-left:0; margin-right:auto; }
|
||||
.vtl:dir(rtl) .vtl-chev:not(.open) { transform:scaleX(-1); }
|
||||
.vtl:dir(rtl) .vtl-collapse-ic svg { transform:scaleX(-1); }
|
||||
.vtl:dir(rtl) .vtl-fab { left:auto; right:calc(12px + env(safe-area-inset-right)); }
|
||||
.vtl:dir(rtl) .vtl-fab { left:auto; right:calc(8px + env(safe-area-inset-right)); }
|
||||
.vtl.rail-collapsed:dir(rtl) .vtl-nav { transform:translateX(184px); }
|
||||
.vtl.rail-collapsed:dir(rtl) .vtl-main { left:0; right:72px; }
|
||||
.vtl.rail-collapsed:dir(rtl) .vtl-collapse { padding-left:10px; padding-right:184px; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue