diff --git a/app/components/home/HomeConsole.vue b/app/components/home/HomeConsole.vue index 7bf900f..f9de6a9 100644 --- a/app/components/home/HomeConsole.vue +++ b/app/components/home/HomeConsole.vue @@ -30,7 +30,7 @@ type="button" role="tab" class="fader" - :class="{ 'fader--active': axis.key === active, 'fader--rising': rising.has(axis.key) }" + :class="{ 'fader--active': axis.key === active, 'fader--held': held.has(axis.key), 'fader--rising': rising.has(axis.key) }" :style="{ '--hue': `var(${axis.hue})`, '--level': levels[axis.key] ?? 0, '--i': i }" :data-axis="axis.key" :aria-selected="axis.key === active" @@ -142,15 +142,20 @@ const current = computed(() => props.axes.find(a => a.key === active.value) ?? p // The visitor's hand takes over for good (03/11): from the first hover, // keyboard focus or tap on a cursor, the rotation stops. The touched cursor // replays its rise, from the bottom to its own graduation, and its panel -// shows. The others do not move; a cursor never touched stays where it was. +// shows. The others are never lowered by the hand: a cursor already touched +// stays at its graduation; a cursor never touched finishes, gently, the move +// the rotation had begun (up to its graduation, or down to its low level), +// then stays there. None stays frozen mid-course. const BEAT_MS = 2600 const BEATS_PER_AXIS = 3 /** Graduation of each axis, from asservissement (0) to émancipation (1) */ const GRADUATION: Record = { numerique: 3 / 4, economie: 1 / 2, politique: 2 / 3 } -/** A focus and a click within this delay are one gesture (a tap): one rise */ +/** The focus and the click of one tap, within this delay, give one rise */ const SAME_GESTURE_MS = 500 const levels = reactive>>({}) +/** Cursors the hand has touched: they no longer glide with the rotation */ +const held = reactive(new Set()) /** Cursors replaying their rise (fast transition) */ const rising = reactive(new Set()) const reduced = ref(false) @@ -160,7 +165,10 @@ const pointerIn = ref(false) const focusIn = ref(false) const hoverKey = ref(null) let lastPointer = '' -const lastRise: Partial> = {} +/** The tab the arrow keys are moving the focus to: its focus is the same gesture */ +let keyFocus: AxisKey | null = null +/** When a keyboard focus last raised each cursor (to pair it with the click of a tap) */ +const focusRiseAt: Partial> = {} const riseRun: Partial> = {} let beat = 0 let timer: ReturnType | undefined @@ -180,24 +188,17 @@ function faderEl(key: AxisKey) { return root.value?.querySelector(`[data-axis="${key}"]`) ?? null } -/** Level currently drawn (mid-transition included), read from the DOM */ -function drawnLevel(key: AxisKey) { - const el = faderEl(key) - const v = el ? Number.parseFloat(getComputedStyle(el).getPropertyValue('--level')) : Number.NaN - return Number.isFinite(v) ? v : (levels[key] ?? 0) -} - -/** First gesture: the rotation stops for good, every cursor stays where it is drawn */ +/** First gesture: the rotation stops for good. The untouched cursors keep the target the + * rotation gave them and finish their glide to it (no freeze mid-course) */ function engage() { if (engaged.value) return - for (const axis of props.axes) levels[axis.key] = drawnLevel(axis.key) engaged.value = true clearInterval(timer) } /** The touched cursor replays its rise: back to the bottom, then up to its graduation */ async function rise(key: AxisKey) { - lastRise[key] = Date.now() + held.add(key) const target = GRADUATION[key] ?? 0.75 if (reduced.value) { levels[key] = target @@ -216,14 +217,20 @@ async function rise(key: AxisKey) { levels[key] = target } -/** A cursor under the visitor's hand: it rises and its panel shows */ +/** A cursor under the visitor's hand: it rises and its panel shows. Every rollover replays + * the rise, however soon after the previous one (Yvv, 03/11) */ function touch(key: AxisKey) { engage() active.value = key - if (Date.now() - (lastRise[key] ?? 0) < SAME_GESTURE_MS) return rise(key) } +/** The panel of a cursor already raised by this same gesture: no second rise */ +function select(key: AxisKey) { + engage() + active.value = key +} + function onEnter(e: PointerEvent, key: AxisKey) { // A finger scrolling over a cursor is not a rollover: taps go through the click if (e.pointerType === 'touch') return @@ -241,19 +248,24 @@ function isKeyboardFocus(el: HTMLElement) { } function onFocus(e: FocusEvent, key: AxisKey) { + // The arrow key that moved the focus here already raised the cursor + if (keyFocus === key) return // Keyboard focus is a rollover; a focus from the mouse or a tap is handled elsewhere if (hoverKey.value === key || !isKeyboardFocus(e.currentTarget as HTMLElement)) return + focusRiseAt[key] = Date.now() touch(key) } function onClick(e: MouseEvent, key: AxisKey) { const pointer = (e as PointerEvent).pointerType || lastPointer lastPointer = '' - if (pointer === 'touch') touch(key) - else { - engage() - active.value = key - } + // A tap is a rollover: one rise per tap, even when the browser also gave it a keyboard focus + const tapFocused = Date.now() - (focusRiseAt[key] ?? 0) < SAME_GESTURE_MS + if (pointer === 'touch' && !tapFocused) return touch(key) + // A click with no hover nor focus before it (a screen reader, a script) raises the cursor + // with its panel; after a hover or a keyboard focus, the cursor is already up + if (hoverKey.value !== key && !held.has(key)) return touch(key) + select(key) } function onFocusOut(e: FocusEvent) { @@ -281,15 +293,23 @@ function onKeydown(e: KeyboardEvent) { const keys = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'] if (!keys.includes(e.key)) return e.preventDefault() - const idx = props.axes.findIndex(a => a.key === active.value) + // From the tab that has the focus: a hover may have selected another axis meanwhile + const from = (e.target as HTMLElement | null)?.closest('[data-axis]')?.dataset.axis + const idx = props.axes.findIndex(a => a.key === (from ?? active.value)) const last = props.axes.length - 1 let next = idx if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = idx === last ? 0 : idx + 1 if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = idx === 0 ? last : idx - 1 if (e.key === 'Home') next = 0 if (e.key === 'End') next = last - touch(props.axes[next]!.key) - nextTick(() => tabs.value[next]?.focus()) + const key = props.axes[next]!.key + touch(key) + nextTick(() => { + // focus() fires the focus event at once: it is part of this key press, not a new rise + keyFocus = key + tabs.value[next]?.focus() + keyFocus = null + }) } onMounted(() => { @@ -383,12 +403,13 @@ onMounted(() => { transition-delay: calc(var(--i) * 160ms); } -/* The visitor's hand has taken over: nothing glides any more but the touched cursor */ -.console--hand .fader { +/* A cursor the hand has touched no longer glides: it drops to the bottom at once, then + replays its rise. The untouched ones finish their glide, then the rotation is over */ +.fader--held { transition: none; } -.console--hand .fader--rising { +.fader--held.fader--rising { transition: --level 1.1s cubic-bezier(0.22, 1, 0.36, 1); } @@ -657,7 +678,7 @@ onMounted(() => { @media (prefers-reduced-motion: reduce) { .fader, - .console--hand .fader--rising, + .fader--held.fader--rising, .fader-fill, .fader-name, .fader-lever, diff --git a/app/components/layout/TheFooter.vue b/app/components/layout/TheFooter.vue index f160d88..bdfc839 100644 --- a/app/components/layout/TheFooter.vue +++ b/app/components/layout/TheFooter.vue @@ -10,9 +10,9 @@

-