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 @@