new landscape styling & scripting for gameroom #id_tray apparatus, & some overall scripting & styling like wobble on click-to-close; new --undUser & --duoUser rootvars universally the table felt values; many new Jasmine tests to handle tray functionality
This commit is contained in:
@@ -1,28 +1,77 @@
|
|||||||
var Tray = (function () {
|
var Tray = (function () {
|
||||||
var _open = false;
|
var _open = false;
|
||||||
var _dragStartX = null;
|
var _dragStartX = null;
|
||||||
|
var _dragStartY = null;
|
||||||
var _dragStartLeft = null;
|
var _dragStartLeft = null;
|
||||||
|
var _dragStartTop = null;
|
||||||
var _dragHandled = false;
|
var _dragHandled = false;
|
||||||
|
|
||||||
var _wrap = null;
|
var _wrap = null;
|
||||||
var _btn = null;
|
var _btn = null;
|
||||||
var _tray = null;
|
var _tray = null;
|
||||||
|
|
||||||
|
// Portrait bounds (X axis)
|
||||||
var _minLeft = 0;
|
var _minLeft = 0;
|
||||||
var _maxLeft = 0;
|
var _maxLeft = 0;
|
||||||
|
|
||||||
// Stored so reset() can remove them from document
|
// Landscape bounds (Y axis): _maxTop = closed (more negative), _minTop = open
|
||||||
var _onDocMove = null;
|
var _minTop = 0;
|
||||||
var _onDocUp = null;
|
var _maxTop = 0;
|
||||||
|
|
||||||
|
// Stored so reset() can remove them
|
||||||
|
var _onDocMove = null;
|
||||||
|
var _onDocUp = null;
|
||||||
|
var _onBtnClick = null;
|
||||||
|
var _closePendingHide = null; // portrait: pending display:none after slide
|
||||||
|
|
||||||
|
function _cancelPendingHide() {
|
||||||
|
if (_closePendingHide && _wrap) {
|
||||||
|
_wrap.removeEventListener('transitionend', _closePendingHide);
|
||||||
|
}
|
||||||
|
_closePendingHide = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testing hook — null means use real window dimensions
|
||||||
|
var _landscapeOverride = null;
|
||||||
|
|
||||||
|
function _isLandscape() {
|
||||||
|
if (_landscapeOverride !== null) return _landscapeOverride;
|
||||||
|
return window.innerWidth > window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
function _computeBounds() {
|
function _computeBounds() {
|
||||||
var rightPx = parseInt(getComputedStyle(_wrap).right, 10);
|
if (_isLandscape()) {
|
||||||
if (isNaN(rightPx)) rightPx = 0;
|
// Landscape: the wrap slides on the Y axis.
|
||||||
var handleW = _btn.offsetWidth || 48;
|
// Structure (column-reverse): tray above, handle below.
|
||||||
_minLeft = 0;
|
// Tray is always display:block in landscape — wrap top hides/reveals it.
|
||||||
_maxLeft = window.innerWidth - rightPx - handleW;
|
// Closed: wrap top = -(trayH) so tray is above viewport, handle at y=0.
|
||||||
|
// Open: wrap top = gearBtnTop - wrapH so handle bottom = gear btn top.
|
||||||
|
var gearBtn = document.getElementById('id_gear_btn');
|
||||||
|
var gearBtnTop = window.innerHeight;
|
||||||
|
if (gearBtn) {
|
||||||
|
gearBtnTop = Math.round(gearBtn.getBoundingClientRect().top);
|
||||||
|
}
|
||||||
|
var handleH = (_btn && _btn.offsetHeight) || 48;
|
||||||
|
// Tray is display:block so offsetHeight includes it; fall back to 280.
|
||||||
|
var wrapH = (_wrap && _wrap.offsetHeight) || (handleH + 280);
|
||||||
|
|
||||||
|
// Closed: tray hidden above viewport, handle visible at y=0.
|
||||||
|
_maxTop = -(wrapH - handleH);
|
||||||
|
|
||||||
|
// Open: handle bottom at gear btn top.
|
||||||
|
_minTop = gearBtnTop - wrapH;
|
||||||
|
} else {
|
||||||
|
// Portrait: slide on X axis.
|
||||||
|
var rightPx = parseInt(getComputedStyle(_wrap).right, 10);
|
||||||
|
if (isNaN(rightPx)) rightPx = 0;
|
||||||
|
var handleW = _btn.offsetWidth || 48;
|
||||||
|
_minLeft = 0;
|
||||||
|
_maxLeft = window.innerWidth - rightPx - handleW;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _applyVerticalBounds() {
|
function _applyVerticalBounds() {
|
||||||
|
// Portrait only: nudge wrap top/bottom to avoid overlapping nav/footer bars.
|
||||||
var nav = document.querySelector('nav');
|
var nav = document.querySelector('nav');
|
||||||
var footer = document.querySelector('footer');
|
var footer = document.querySelector('footer');
|
||||||
var rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
var rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
@@ -43,42 +92,93 @@ var Tray = (function () {
|
|||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
if (_open) return;
|
if (_open) return;
|
||||||
|
_cancelPendingHide(); // abort any in-flight portrait close animation
|
||||||
_open = true;
|
_open = true;
|
||||||
if (_tray) _tray.style.display = 'block';
|
// Portrait only: toggle tray display.
|
||||||
|
// Landscape: tray is always display:block; wrap position controls visibility.
|
||||||
|
if (!_isLandscape() && _tray) _tray.style.display = 'block';
|
||||||
if (_btn) _btn.classList.add('open');
|
if (_btn) _btn.classList.add('open');
|
||||||
if (_wrap) {
|
if (_wrap) {
|
||||||
_wrap.classList.remove('tray-dragging');
|
_wrap.classList.remove('tray-dragging');
|
||||||
_wrap.style.left = _minLeft + 'px';
|
if (_isLandscape()) {
|
||||||
|
_wrap.style.top = _minTop + 'px';
|
||||||
|
} else {
|
||||||
|
_wrap.style.left = _minLeft + 'px';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
if (!_open) return;
|
if (!_open) return;
|
||||||
_open = false;
|
_open = false;
|
||||||
if (_tray) _tray.style.display = 'none';
|
|
||||||
if (_btn) _btn.classList.remove('open');
|
if (_btn) _btn.classList.remove('open');
|
||||||
if (_wrap) {
|
if (_wrap) {
|
||||||
_wrap.classList.remove('tray-dragging');
|
_wrap.classList.remove('tray-dragging');
|
||||||
_wrap.style.left = _maxLeft + 'px';
|
if (_isLandscape()) {
|
||||||
|
_wrap.style.top = _maxTop + 'px';
|
||||||
|
// Snap after the slide completes.
|
||||||
|
_closePendingHide = function (e) {
|
||||||
|
if (e.propertyName !== 'top') return;
|
||||||
|
if (_wrap) _wrap.removeEventListener('transitionend', _closePendingHide);
|
||||||
|
_closePendingHide = null;
|
||||||
|
_snapWrap();
|
||||||
|
};
|
||||||
|
_wrap.addEventListener('transitionend', _closePendingHide);
|
||||||
|
} else {
|
||||||
|
_wrap.style.left = _maxLeft + 'px';
|
||||||
|
// Snap first (tray still visible so it peeks), then hide tray.
|
||||||
|
_closePendingHide = function (e) {
|
||||||
|
if (e.propertyName !== 'left') return;
|
||||||
|
if (_wrap) _wrap.removeEventListener('transitionend', _closePendingHide);
|
||||||
|
_closePendingHide = null;
|
||||||
|
_snapWrap(function () {
|
||||||
|
if (!_open && _tray) _tray.style.display = 'none';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
_wrap.addEventListener('transitionend', _closePendingHide);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOpen() { return _open; }
|
function isOpen() { return _open; }
|
||||||
|
|
||||||
|
function _snapWrap(onDone) {
|
||||||
|
if (!_wrap) return;
|
||||||
|
_wrap.classList.add('snap');
|
||||||
|
_wrap.addEventListener('animationend', function handler() {
|
||||||
|
if (_wrap) _wrap.classList.remove('snap');
|
||||||
|
_wrap.removeEventListener('animationend', handler);
|
||||||
|
if (onDone) onDone();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function _wobble() {
|
function _wobble() {
|
||||||
if (!_wrap) return;
|
if (!_wrap) return;
|
||||||
|
// Portrait: show tray so it peeks in during the translateX animation,
|
||||||
|
// then re-hide it if the tray is still closed when the animation ends.
|
||||||
|
if (!_isLandscape() && _tray) _tray.style.display = 'block';
|
||||||
_wrap.classList.add('wobble');
|
_wrap.classList.add('wobble');
|
||||||
_wrap.addEventListener('animationend', function handler() {
|
_wrap.addEventListener('animationend', function handler() {
|
||||||
_wrap.classList.remove('wobble');
|
_wrap.classList.remove('wobble');
|
||||||
_wrap.removeEventListener('animationend', handler);
|
_wrap.removeEventListener('animationend', handler);
|
||||||
|
if (!_isLandscape() && !_open && _tray) _tray.style.display = 'none';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _startDrag(clientX) {
|
function _startDrag(clientX, clientY) {
|
||||||
_dragStartX = clientX;
|
_dragHandled = false;
|
||||||
_dragStartLeft = _wrap ? (parseInt(_wrap.style.left, 10) || _maxLeft) : _maxLeft;
|
|
||||||
_dragHandled = false;
|
|
||||||
if (_wrap) _wrap.classList.add('tray-dragging');
|
if (_wrap) _wrap.classList.add('tray-dragging');
|
||||||
|
if (_isLandscape()) {
|
||||||
|
_dragStartY = clientY;
|
||||||
|
_dragStartX = null;
|
||||||
|
_dragStartTop = _wrap ? (parseInt(_wrap.style.top, 10) || _maxTop) : _maxTop;
|
||||||
|
_dragStartLeft = null;
|
||||||
|
} else {
|
||||||
|
_dragStartX = clientX;
|
||||||
|
_dragStartY = null;
|
||||||
|
_dragStartLeft = _wrap ? (parseInt(_wrap.style.left, 10) || _maxLeft) : _maxLeft;
|
||||||
|
_dragStartTop = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@@ -87,40 +187,72 @@ var Tray = (function () {
|
|||||||
_tray = document.getElementById('id_tray');
|
_tray = document.getElementById('id_tray');
|
||||||
if (!_btn) return;
|
if (!_btn) return;
|
||||||
|
|
||||||
_computeBounds();
|
if (_isLandscape()) {
|
||||||
_applyVerticalBounds();
|
// Show tray before measuring so offsetHeight includes it.
|
||||||
if (_wrap) _wrap.style.left = _maxLeft + 'px';
|
if (_tray) _tray.style.display = 'block';
|
||||||
|
_computeBounds();
|
||||||
|
// Clear portrait's inline left/bottom so media-query CSS applies.
|
||||||
|
if (_wrap) { _wrap.style.left = ''; _wrap.style.bottom = ''; }
|
||||||
|
if (_wrap) _wrap.style.top = _maxTop + 'px';
|
||||||
|
} else {
|
||||||
|
// Clear landscape's inline top so portrait CSS applies.
|
||||||
|
if (_wrap) _wrap.style.top = '';
|
||||||
|
_applyVerticalBounds();
|
||||||
|
_computeBounds();
|
||||||
|
if (_wrap) _wrap.style.left = _maxLeft + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
// Drag start — pointer and mouse variants so Selenium W3C actions
|
// Drag start — pointer and mouse variants so Selenium W3C actions
|
||||||
// and synthetic Jasmine PointerEvents both work.
|
// and synthetic Jasmine PointerEvents both work.
|
||||||
_btn.addEventListener('pointerdown', function (e) {
|
_btn.addEventListener('pointerdown', function (e) {
|
||||||
_startDrag(e.clientX);
|
_startDrag(e.clientX, e.clientY);
|
||||||
try { _btn.setPointerCapture(e.pointerId); } catch (_) {}
|
try { _btn.setPointerCapture(e.pointerId); } catch (_) {}
|
||||||
});
|
});
|
||||||
_btn.addEventListener('mousedown', function (e) {
|
_btn.addEventListener('mousedown', function (e) {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
if (_dragStartX !== null) return; // pointerdown already handled it
|
if (_dragStartX !== null || _dragStartY !== null) return;
|
||||||
_startDrag(e.clientX);
|
_startDrag(e.clientX, e.clientY);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Drag move / end — on document so events that land elsewhere during
|
// Drag move / end — on document so events that land elsewhere during
|
||||||
// the drag (no capture, or Selenium pointer quirks) still bubble here.
|
// the drag (no capture, or Selenium pointer quirks) still bubble here.
|
||||||
_onDocMove = function (e) {
|
_onDocMove = function (e) {
|
||||||
if (_dragStartX === null || !_wrap) return;
|
if (!_wrap) return;
|
||||||
var newLeft = _dragStartLeft + (e.clientX - _dragStartX);
|
if (_isLandscape()) {
|
||||||
newLeft = Math.max(_minLeft, Math.min(_maxLeft, newLeft));
|
if (_dragStartY === null) return;
|
||||||
_wrap.style.left = newLeft + 'px';
|
var newTop = _dragStartTop + (e.clientY - _dragStartY);
|
||||||
if (newLeft < _maxLeft) {
|
newTop = Math.max(_maxTop, Math.min(_minTop, newTop));
|
||||||
if (!_open) {
|
_wrap.style.top = newTop + 'px';
|
||||||
_open = true;
|
// Open when dragged below closed position; update state + class only.
|
||||||
if (_tray) _tray.style.display = 'block';
|
// Tray display is not toggled in landscape — position controls visibility.
|
||||||
if (_btn) _btn.classList.add('open');
|
if (newTop > _maxTop) {
|
||||||
|
if (!_open) {
|
||||||
|
_open = true;
|
||||||
|
if (_btn) _btn.classList.add('open');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_open) {
|
||||||
|
_open = false;
|
||||||
|
if (_btn) _btn.classList.remove('open');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_open) {
|
if (_dragStartX === null) return;
|
||||||
_open = false;
|
var newLeft = _dragStartLeft + (e.clientX - _dragStartX);
|
||||||
if (_tray) _tray.style.display = 'none';
|
newLeft = Math.max(_minLeft, Math.min(_maxLeft, newLeft));
|
||||||
if (_btn) _btn.classList.remove('open');
|
_wrap.style.left = newLeft + 'px';
|
||||||
|
if (newLeft < _maxLeft) {
|
||||||
|
if (!_open) {
|
||||||
|
_open = true;
|
||||||
|
if (_tray) _tray.style.display = 'block';
|
||||||
|
if (_btn) _btn.classList.add('open');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_open) {
|
||||||
|
_open = false;
|
||||||
|
if (_tray) _tray.style.display = 'none';
|
||||||
|
if (_btn) _btn.classList.remove('open');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -128,17 +260,25 @@ var Tray = (function () {
|
|||||||
document.addEventListener('mousemove', _onDocMove);
|
document.addEventListener('mousemove', _onDocMove);
|
||||||
|
|
||||||
_onDocUp = function (e) {
|
_onDocUp = function (e) {
|
||||||
if (_dragStartX !== null && Math.abs(e.clientX - _dragStartX) > 10) {
|
if (_isLandscape()) {
|
||||||
_dragHandled = true;
|
if (_dragStartY !== null && Math.abs(e.clientY - _dragStartY) > 10) {
|
||||||
|
_dragHandled = true;
|
||||||
|
}
|
||||||
|
_dragStartY = null;
|
||||||
|
_dragStartTop = null;
|
||||||
|
} else {
|
||||||
|
if (_dragStartX !== null && Math.abs(e.clientX - _dragStartX) > 10) {
|
||||||
|
_dragHandled = true;
|
||||||
|
}
|
||||||
|
_dragStartX = null;
|
||||||
|
_dragStartLeft = null;
|
||||||
}
|
}
|
||||||
_dragStartX = null;
|
|
||||||
_dragStartLeft = null;
|
|
||||||
if (_wrap) _wrap.classList.remove('tray-dragging');
|
if (_wrap) _wrap.classList.remove('tray-dragging');
|
||||||
};
|
};
|
||||||
document.addEventListener('pointerup', _onDocUp);
|
document.addEventListener('pointerup', _onDocUp);
|
||||||
document.addEventListener('mouseup', _onDocUp);
|
document.addEventListener('mouseup', _onDocUp);
|
||||||
|
|
||||||
_btn.addEventListener('click', function () {
|
_onBtnClick = function () {
|
||||||
if (_dragHandled) {
|
if (_dragHandled) {
|
||||||
_dragHandled = false;
|
_dragHandled = false;
|
||||||
return;
|
return;
|
||||||
@@ -148,26 +288,43 @@ var Tray = (function () {
|
|||||||
} else {
|
} else {
|
||||||
_wobble();
|
_wobble();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
_btn.addEventListener('click', _onBtnClick);
|
||||||
|
|
||||||
window.addEventListener('resize', function () {
|
window.addEventListener('resize', function () {
|
||||||
_computeBounds();
|
if (_isLandscape()) {
|
||||||
_applyVerticalBounds();
|
// Ensure tray is visible before measuring bounds.
|
||||||
if (!_open && _wrap) _wrap.style.left = _maxLeft + 'px';
|
if (_tray) _tray.style.display = 'block';
|
||||||
|
if (_wrap) { _wrap.style.left = ''; _wrap.style.bottom = ''; }
|
||||||
|
_computeBounds();
|
||||||
|
if (!_open && _wrap) _wrap.style.top = _maxTop + 'px';
|
||||||
|
} else {
|
||||||
|
// Switching to portrait: hide tray if closed.
|
||||||
|
if (!_open && _tray) _tray.style.display = 'none';
|
||||||
|
if (_wrap) _wrap.style.top = '';
|
||||||
|
_computeBounds();
|
||||||
|
_applyVerticalBounds();
|
||||||
|
if (!_open && _wrap) _wrap.style.left = _maxLeft + 'px';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset() — restores module state; used by Jasmine afterEach
|
// reset() — restores module state; used by Jasmine afterEach
|
||||||
function reset() {
|
function reset() {
|
||||||
_open = false;
|
_open = false;
|
||||||
_dragStartX = null;
|
_dragStartX = null;
|
||||||
_dragStartLeft = null;
|
_dragStartY = null;
|
||||||
_dragHandled = false;
|
_dragStartLeft = null;
|
||||||
|
_dragStartTop = null;
|
||||||
|
_dragHandled = false;
|
||||||
|
_landscapeOverride = null;
|
||||||
|
// Restore portrait default (display:none); landscape init() will show it.
|
||||||
if (_tray) _tray.style.display = 'none';
|
if (_tray) _tray.style.display = 'none';
|
||||||
if (_btn) _btn.classList.remove('open');
|
if (_btn) _btn.classList.remove('open');
|
||||||
if (_wrap) {
|
if (_wrap) {
|
||||||
_wrap.classList.remove('wobble', 'tray-dragging');
|
_wrap.classList.remove('wobble', 'snap', 'tray-dragging');
|
||||||
_wrap.style.left = '';
|
_wrap.style.left = '';
|
||||||
|
_wrap.style.top = '';
|
||||||
}
|
}
|
||||||
if (_onDocMove) {
|
if (_onDocMove) {
|
||||||
document.removeEventListener('pointermove', _onDocMove);
|
document.removeEventListener('pointermove', _onDocMove);
|
||||||
@@ -179,6 +336,11 @@ var Tray = (function () {
|
|||||||
document.removeEventListener('mouseup', _onDocUp);
|
document.removeEventListener('mouseup', _onDocUp);
|
||||||
_onDocUp = null;
|
_onDocUp = null;
|
||||||
}
|
}
|
||||||
|
if (_onBtnClick && _btn) {
|
||||||
|
_btn.removeEventListener('click', _onBtnClick);
|
||||||
|
_onBtnClick = null;
|
||||||
|
}
|
||||||
|
_cancelPendingHide();
|
||||||
_wrap = null;
|
_wrap = null;
|
||||||
_btn = null;
|
_btn = null;
|
||||||
_tray = null;
|
_tray = null;
|
||||||
@@ -190,5 +352,12 @@ var Tray = (function () {
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { init: init, open: open, close: close, isOpen: isOpen, reset: reset };
|
return {
|
||||||
|
init: init,
|
||||||
|
open: open,
|
||||||
|
close: close,
|
||||||
|
isOpen: isOpen,
|
||||||
|
reset: reset,
|
||||||
|
_testSetLandscape: function (v) { _landscapeOverride = v; },
|
||||||
|
};
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ class TrayTest(FunctionalTest):
|
|||||||
document.dispatchEvent(new PointerEvent("pointerup", {clientX: endX, bubbles: true}));
|
document.dispatchEvent(new PointerEvent("pointerup", {clientX: endX, bubbles: true}));
|
||||||
""", btn, start_x, end_x)
|
""", btn, start_x, end_x)
|
||||||
|
|
||||||
|
def _simulate_drag_y(self, btn, offset_y):
|
||||||
|
"""Dispatch JS pointer events on the Y axis for landscape drag tests."""
|
||||||
|
start_y = btn.rect['y'] + btn.rect['height'] / 2
|
||||||
|
end_y = start_y + offset_y
|
||||||
|
self.browser.execute_script("""
|
||||||
|
var btn = arguments[0], startY = arguments[1], endY = arguments[2];
|
||||||
|
btn.dispatchEvent(new PointerEvent("pointerdown", {clientY: startY, clientX: 0, bubbles: true}));
|
||||||
|
document.dispatchEvent(new PointerEvent("pointermove", {clientY: endY, clientX: 0, bubbles: true}));
|
||||||
|
document.dispatchEvent(new PointerEvent("pointerup", {clientY: endY, clientX: 0, bubbles: true}));
|
||||||
|
""", btn, start_y, end_y)
|
||||||
|
|
||||||
def _make_sig_select_room(self, founder_email="founder@test.io"):
|
def _make_sig_select_room(self, founder_email="founder@test.io"):
|
||||||
founder, _ = User.objects.get_or_create(email=founder_email)
|
founder, _ = User.objects.get_or_create(email=founder_email)
|
||||||
room = Room.objects.create(name="Tray Test Room", owner=founder)
|
room = Room.objects.create(name="Tray Test Room", owner=founder)
|
||||||
@@ -155,3 +166,45 @@ class TrayTest(FunctionalTest):
|
|||||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
||||||
tray = self.browser.find_element(By.ID, "id_tray")
|
tray = self.browser.find_element(By.ID, "id_tray")
|
||||||
self.assertFalse(tray.is_displayed())
|
self.assertFalse(tray.is_displayed())
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Test T6 — landscape: tray btn is near the top edge of the viewport #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def test_tray_btn_anchored_near_top_in_landscape(self):
|
||||||
|
room = self._make_sig_select_room()
|
||||||
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
|
self.browser.set_window_size(900, 500)
|
||||||
|
self.browser.get(self._room_url(room))
|
||||||
|
|
||||||
|
btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.ID, "id_tray_btn")
|
||||||
|
)
|
||||||
|
self.assertTrue(btn.is_displayed())
|
||||||
|
|
||||||
|
# In landscape the handle sits at the top of the content area;
|
||||||
|
# btn bottom should be within the top 40% of the viewport.
|
||||||
|
vh = self.browser.execute_script("return window.innerHeight")
|
||||||
|
btn_bottom = btn.location["y"] + btn.size["height"]
|
||||||
|
self.assertLess(btn_bottom, vh * 0.4)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Test T7 — landscape: dragging btn downward opens the tray #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def test_dragging_tray_btn_down_opens_tray_in_landscape(self):
|
||||||
|
room = self._make_sig_select_room()
|
||||||
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
|
self.browser.set_window_size(900, 500)
|
||||||
|
self.browser.get(self._room_url(room))
|
||||||
|
|
||||||
|
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
|
||||||
|
# In landscape, #id_tray is always display:block; position controls visibility.
|
||||||
|
# Use Tray.isOpen() to check logical state.
|
||||||
|
self.assertFalse(self.browser.execute_script("return Tray.isOpen()"))
|
||||||
|
|
||||||
|
self._simulate_drag_y(btn, 300)
|
||||||
|
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertTrue(self.browser.execute_script("return Tray.isOpen()"))
|
||||||
|
)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ describe("Tray", () => {
|
|||||||
document.body.appendChild(wrap);
|
document.body.appendChild(wrap);
|
||||||
document.body.appendChild(tray);
|
document.body.appendChild(tray);
|
||||||
|
|
||||||
|
Tray._testSetLandscape(false); // force portrait regardless of window size
|
||||||
Tray.init();
|
Tray.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,8 +84,10 @@ describe("Tray", () => {
|
|||||||
describe("close()", () => {
|
describe("close()", () => {
|
||||||
beforeEach(() => Tray.open());
|
beforeEach(() => Tray.open());
|
||||||
|
|
||||||
it("hides #id_tray", () => {
|
it("hides #id_tray after the slide transition completes", () => {
|
||||||
Tray.close();
|
Tray.close();
|
||||||
|
// display:none is deferred until transitionend — fire it manually.
|
||||||
|
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
|
||||||
expect(tray.style.display).toBe("none");
|
expect(tray.style.display).toBe("none");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,4 +206,131 @@ describe("Tray", () => {
|
|||||||
expect(wrap.classList.contains("wobble")).toBe(false);
|
expect(wrap.classList.contains("wobble")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
// Landscape mode — Y-axis drag, top-positioned wrap //
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
describe("landscape mode", () => {
|
||||||
|
// Re-init in landscape after the portrait init from outer beforeEach.
|
||||||
|
beforeEach(() => {
|
||||||
|
Tray.reset();
|
||||||
|
Tray._testSetLandscape(true);
|
||||||
|
Tray.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
function simulateDragY(deltaY) {
|
||||||
|
const startY = 50;
|
||||||
|
btn.dispatchEvent(new PointerEvent("pointerdown", { clientY: startY, clientX: 0, bubbles: true }));
|
||||||
|
btn.dispatchEvent(new PointerEvent("pointermove", { clientY: startY + deltaY, clientX: 0, bubbles: true }));
|
||||||
|
btn.dispatchEvent(new PointerEvent("pointerup", { clientY: startY + deltaY, clientX: 0, bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── open() in landscape ─────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("open()", () => {
|
||||||
|
it("makes #id_tray visible", () => {
|
||||||
|
Tray.open();
|
||||||
|
expect(tray.style.display).not.toBe("none");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds .open to #id_tray_btn", () => {
|
||||||
|
Tray.open();
|
||||||
|
expect(btn.classList.contains("open")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("positions wrap via style.top, not style.left", () => {
|
||||||
|
Tray.open();
|
||||||
|
expect(wrap.style.top).not.toBe("");
|
||||||
|
expect(wrap.style.left).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── close() in landscape ────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("close()", () => {
|
||||||
|
beforeEach(() => Tray.open());
|
||||||
|
|
||||||
|
it("closes the tray (display not toggled in landscape)", () => {
|
||||||
|
Tray.close();
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes .open from #id_tray_btn", () => {
|
||||||
|
Tray.close();
|
||||||
|
expect(btn.classList.contains("open")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closed top is less than open top (wrap slides up to close)", () => {
|
||||||
|
const openTop = parseInt(wrap.style.top, 10);
|
||||||
|
Tray.close();
|
||||||
|
const closedTop = parseInt(wrap.style.top, 10);
|
||||||
|
expect(closedTop).toBeLessThan(openTop);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── drag — Y axis ──────────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("drag interaction", () => {
|
||||||
|
it("dragging down opens the tray", () => {
|
||||||
|
simulateDragY(100);
|
||||||
|
expect(Tray.isOpen()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dragging up does not open the tray", () => {
|
||||||
|
simulateDragY(-100);
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drag > 10px downward suppresses subsequent click", () => {
|
||||||
|
simulateDragY(100);
|
||||||
|
btn.click(); // should be swallowed — tray stays open
|
||||||
|
expect(Tray.isOpen()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not set style.left (Y axis only)", () => {
|
||||||
|
simulateDragY(100);
|
||||||
|
expect(wrap.style.left).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not add .wobble during drag", () => {
|
||||||
|
simulateDragY(100);
|
||||||
|
expect(wrap.classList.contains("wobble")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── click when closed — wobble, no open ───────────────────────── //
|
||||||
|
|
||||||
|
describe("clicking btn when closed", () => {
|
||||||
|
it("adds .wobble to wrap", () => {
|
||||||
|
btn.click();
|
||||||
|
expect(wrap.classList.contains("wobble")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not open the tray", () => {
|
||||||
|
btn.click();
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── click when open — close ────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("clicking btn when open", () => {
|
||||||
|
beforeEach(() => Tray.open());
|
||||||
|
|
||||||
|
it("closes the tray", () => {
|
||||||
|
btn.click();
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── init positions wrap at closed (top) ────────────────────────── //
|
||||||
|
|
||||||
|
it("init sets wrap to closed position (top < 0 or = maxTop)", () => {
|
||||||
|
// After landscape init with no real elements, _maxTop = -(wrapH_fallback - handleH_fallback)
|
||||||
|
// which will be negative. Wrap starts off-screen above.
|
||||||
|
const top = parseInt(wrap.style.top, 10);
|
||||||
|
expect(top).toBeLessThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container: fill centre, compensate for fixed sidebars on both sides
|
// Container: fill center, compensate for fixed sidebars on both sides
|
||||||
body .container {
|
body .container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -284,7 +284,7 @@ body {
|
|||||||
margin: 0 0 0.25rem;
|
margin: 0 0 0.25rem;
|
||||||
letter-spacing: 0.4em;
|
letter-spacing: 0.4em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-align-last: center;
|
text-align-last: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,26 +363,6 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @media (min-width: 1024px) and (max-height: 700px) {
|
|
||||||
// body .container .navbar {
|
|
||||||
// padding: 0.5rem 0;
|
|
||||||
|
|
||||||
// .navbar-brand h1 {
|
|
||||||
// font-size: 1.4rem;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #id_footer {
|
|
||||||
// height: 3.5rem;
|
|
||||||
// padding: 0.7rem 1rem;
|
|
||||||
// gap: 0.35rem;
|
|
||||||
|
|
||||||
// #id_footer_nav a {
|
|
||||||
// font-size: 1.2rem;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#id_footer {
|
#id_footer {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
|
|||||||
@@ -371,9 +371,9 @@ $seat-r-y: round($seat-r * 0.5); // 65px
|
|||||||
width: 160px;
|
width: 160px;
|
||||||
height: 185px;
|
height: 185px;
|
||||||
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
||||||
background: rgba(var(--priUser), 0.8);
|
background: rgba(var(--duoUser), 1);
|
||||||
// box-shadow is clipped by clip-path; use filter instead
|
// box-shadow is clipped by clip-path; use filter instead
|
||||||
filter: drop-shadow(0 0 8px rgba(var(--terUser), 0.25));
|
filter: drop-shadow(0 0 8px rgba(var(--duoUser), 1));
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -678,6 +678,7 @@ $inv-strip: 30px; // visible height of each stacked card after the first
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Significator deck (SIG_SELECT phase) ──────────────────────────────────
|
// ─── Significator deck (SIG_SELECT phase) ──────────────────────────────────
|
||||||
@@ -781,7 +782,8 @@ $handle-r: 1rem;
|
|||||||
transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
&.tray-dragging { transition: none; }
|
&.tray-dragging { transition: none; }
|
||||||
&.wobble { animation: tray-wobble 0.45s ease; }
|
&.wobble { animation: tray-wobble 0.45s ease; }
|
||||||
|
&.snap { animation: tray-snap 0.30s ease; }
|
||||||
}
|
}
|
||||||
|
|
||||||
#id_tray_handle {
|
#id_tray_handle {
|
||||||
@@ -865,6 +867,15 @@ $handle-r: 1rem;
|
|||||||
80% { transform: translateX(3px); }
|
80% { transform: translateX(3px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inverted wobble — handle overshoots past the wall on close, then bounces back.
|
||||||
|
@keyframes tray-snap {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
20% { transform: translateX(8px); }
|
||||||
|
40% { transform: translateX(-6px); }
|
||||||
|
60% { transform: translateX(5px); }
|
||||||
|
80% { transform: translateX(-3px); }
|
||||||
|
}
|
||||||
|
|
||||||
#id_tray {
|
#id_tray {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -872,10 +883,10 @@ $handle-r: 1rem;
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1; // above #id_tray_grip pseudo-elements
|
z-index: 1; // above #id_tray_grip pseudo-elements
|
||||||
background: rgba(var(--secUser), 1);
|
background: rgba(var(--duoUser), 1);
|
||||||
border-left:2.5rem solid rgba(var(--terUser), 1);
|
border-left:2.5rem solid rgba(var(--quaUser), 1);
|
||||||
border-top: 2.5rem solid rgba(var(--terUser), 1);
|
border-top: 2.5rem solid rgba(var(--quaUser), 1);
|
||||||
border-bottom: 2.5rem solid rgba(var(--terUser), 1);
|
border-bottom: 2.5rem solid rgba(var(--quaUser), 1);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
-0.25rem 0 0.5rem rgba(0, 0, 0, 0.75),
|
-0.25rem 0 0.5rem rgba(0, 0, 0, 0.75),
|
||||||
inset 0 0 0 0.12rem rgba(255, 255, 255, 0.12), // bright bevel ring at wall edge
|
inset 0 0 0 0.12rem rgba(255, 255, 255, 0.12), // bright bevel ring at wall edge
|
||||||
@@ -884,6 +895,98 @@ $handle-r: 1rem;
|
|||||||
inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 0.3) // bottom wall depth
|
inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 0.3) // bottom wall depth
|
||||||
;
|
;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
max-height: 85vh; // cap on very tall portrait screens
|
||||||
// scrollbar-width: thin;
|
// scrollbar-width: thin;
|
||||||
// scrollbar-color: rgba(var(--terUser), 0.3) transparent;
|
// scrollbar-color: rgba(var(--terUser), 0.3) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Tray: landscape reorientation ─────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Must come AFTER the portrait tray rules above to win the cascade
|
||||||
|
// (same specificity — later declaration wins).
|
||||||
|
//
|
||||||
|
// In landscape the tray slides DOWN from the top instead of in from the right.
|
||||||
|
// Structure (column-reverse): tray panel above, handle below.
|
||||||
|
// JS controls style.top for the Y-axis slide:
|
||||||
|
// Closed: top = -(trayH) → only handle visible at y = 0
|
||||||
|
// Open: top = gearBtnTop - wrapH → handle bottom at gear btn top
|
||||||
|
//
|
||||||
|
// The wrap fits horizontally between the fixed left-nav and right-footer sidebars.
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
$sidebar-w: 4rem;
|
||||||
|
$tray-landscape-max-w: 960px; // cap tray width on very wide screens
|
||||||
|
|
||||||
|
#id_tray_wrap {
|
||||||
|
flex-direction: column-reverse; // tray panel above, handle below
|
||||||
|
left: $sidebar-w;
|
||||||
|
right: $sidebar-w;
|
||||||
|
top: auto; // JS controls style.top for the Y-axis slide
|
||||||
|
bottom: auto;
|
||||||
|
transition: top 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&.tray-dragging { transition: none; }
|
||||||
|
&.wobble { animation: tray-wobble-landscape 0.45s ease; }
|
||||||
|
&.snap { animation: tray-snap-landscape 0.30s ease; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_tray_handle {
|
||||||
|
width: auto; // full width of wrap
|
||||||
|
height: 48px; // $handle-exposed — same exposed dimension as portrait
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_tray_grip {
|
||||||
|
// Rotate 90°: centred horizontally, extends vertically.
|
||||||
|
// bottom mirrors portrait's left: grip starts at handle centre and extends
|
||||||
|
// toward the tray (upward in column-reverse layout).
|
||||||
|
bottom: calc(48px / 2 - 0.125rem); // $handle-exposed / 2 from handle bottom
|
||||||
|
top: auto;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 72px; // $handle-rect-h — narrow visible dimension
|
||||||
|
height: 10000px; // $handle-rect-w — extends upward into tray area
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_tray {
|
||||||
|
// Borders: left/right/bottom are visible walls; top edge is open.
|
||||||
|
// Bottom faces the handle (same logic as portrait's left border facing handle).
|
||||||
|
border-left: 2.5rem solid rgba(var(--quaUser), 1);
|
||||||
|
border-right: 2.5rem solid rgba(var(--quaUser), 1);
|
||||||
|
border-bottom: 2.5rem solid rgba(var(--quaUser), 1);
|
||||||
|
border-top: none;
|
||||||
|
|
||||||
|
margin-left: 0; // portrait horizontal gap no longer needed
|
||||||
|
margin-bottom: 0.5rem; // gap between tray bottom and handle top
|
||||||
|
|
||||||
|
// Cap width on ultra-wide screens; center within the handle shelf.
|
||||||
|
width: 100%;
|
||||||
|
max-width: $tray-landscape-max-w;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
box-shadow:
|
||||||
|
0 0.25rem 0.5rem rgba(0, 0, 0, 0.75), // outer shadow (downward, below tray toward handle)
|
||||||
|
inset 0 0 0 0.12rem rgba(255, 255, 255, 0.12), // bright bevel ring
|
||||||
|
inset 0 -0.6rem 1.5rem -0.5rem rgba(0, 0, 0, 0.45), // bottom wall depth (inward from bottom border)
|
||||||
|
inset 0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 0.3), // left wall depth
|
||||||
|
inset -0.6rem 0 1.5rem -0.5rem rgba(0, 0, 0, 0.3) // right wall depth
|
||||||
|
;
|
||||||
|
min-height: 2000px; // give tray real height so JS offsetHeight > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tray-wobble-landscape {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
20% { transform: translateY(-8px); }
|
||||||
|
40% { transform: translateY(6px); }
|
||||||
|
60% { transform: translateY(-5px); }
|
||||||
|
80% { transform: translateY(3px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverted wobble — wrap overshoots upward on close, then bounces back.
|
||||||
|
@keyframes tray-snap-landscape {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
20% { transform: translateY(8px); }
|
||||||
|
40% { transform: translateY(-6px); }
|
||||||
|
60% { transform: translateY(5px); }
|
||||||
|
80% { transform: translateY(-3px); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -199,9 +199,9 @@
|
|||||||
--secPmm: 150, 120, 182;
|
--secPmm: 150, 120, 182;
|
||||||
--terPmm: 112, 79, 146;
|
--terPmm: 112, 79, 146;
|
||||||
// forest
|
// forest
|
||||||
--priFor: 190, 209, 170;
|
--priFor: 114, 146, 79;
|
||||||
--secFor: 152, 182, 120;
|
--secFor: 94, 124, 61;
|
||||||
--terFor: 114, 146, 79;
|
--terFor: 74, 102, 43;
|
||||||
|
|
||||||
/* Technoman Palette */
|
/* Technoman Palette */
|
||||||
// carbon steel
|
// carbon steel
|
||||||
@@ -302,6 +302,10 @@
|
|||||||
// • pure (rare)
|
// • pure (rare)
|
||||||
--ninClh: 192, 77, 1;
|
--ninClh: 192, 77, 1;
|
||||||
--decClh: 255, 174, 0;
|
--decClh: 255, 174, 0;
|
||||||
|
|
||||||
|
// Felt values
|
||||||
|
--undUser: var(--priFor);
|
||||||
|
--duoUser: var(--terFor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Default Earthman Palette */
|
/* Default Earthman Palette */
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ describe("Tray", () => {
|
|||||||
document.body.appendChild(wrap);
|
document.body.appendChild(wrap);
|
||||||
document.body.appendChild(tray);
|
document.body.appendChild(tray);
|
||||||
|
|
||||||
|
Tray._testSetLandscape(false); // force portrait regardless of window size
|
||||||
Tray.init();
|
Tray.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,11 +84,26 @@ describe("Tray", () => {
|
|||||||
describe("close()", () => {
|
describe("close()", () => {
|
||||||
beforeEach(() => Tray.open());
|
beforeEach(() => Tray.open());
|
||||||
|
|
||||||
it("hides #id_tray", () => {
|
it("hides #id_tray after slide + snap both complete", () => {
|
||||||
Tray.close();
|
Tray.close();
|
||||||
|
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
|
||||||
|
wrap.dispatchEvent(new Event("animationend"));
|
||||||
expect(tray.style.display).toBe("none");
|
expect(tray.style.display).toBe("none");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds .snap to wrap after slide transition completes", () => {
|
||||||
|
Tray.close();
|
||||||
|
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
|
||||||
|
expect(wrap.classList.contains("snap")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes .snap from wrap once animationend fires", () => {
|
||||||
|
Tray.close();
|
||||||
|
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "left" }));
|
||||||
|
wrap.dispatchEvent(new Event("animationend"));
|
||||||
|
expect(wrap.classList.contains("snap")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("removes .open from #id_tray_btn", () => {
|
it("removes .open from #id_tray_btn", () => {
|
||||||
Tray.close();
|
Tray.close();
|
||||||
expect(btn.classList.contains("open")).toBe(false);
|
expect(btn.classList.contains("open")).toBe(false);
|
||||||
@@ -203,4 +219,144 @@ describe("Tray", () => {
|
|||||||
expect(wrap.classList.contains("wobble")).toBe(false);
|
expect(wrap.classList.contains("wobble")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
// Landscape mode — Y-axis drag, top-positioned wrap //
|
||||||
|
// ---------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
describe("landscape mode", () => {
|
||||||
|
// Re-init in landscape after the portrait init from outer beforeEach.
|
||||||
|
beforeEach(() => {
|
||||||
|
Tray.reset();
|
||||||
|
Tray._testSetLandscape(true);
|
||||||
|
Tray.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
function simulateDragY(deltaY) {
|
||||||
|
const startY = 50;
|
||||||
|
btn.dispatchEvent(new PointerEvent("pointerdown", { clientY: startY, clientX: 0, bubbles: true }));
|
||||||
|
btn.dispatchEvent(new PointerEvent("pointermove", { clientY: startY + deltaY, clientX: 0, bubbles: true }));
|
||||||
|
btn.dispatchEvent(new PointerEvent("pointerup", { clientY: startY + deltaY, clientX: 0, bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── open() in landscape ─────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("open()", () => {
|
||||||
|
it("makes #id_tray visible", () => {
|
||||||
|
Tray.open();
|
||||||
|
expect(tray.style.display).not.toBe("none");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds .open to #id_tray_btn", () => {
|
||||||
|
Tray.open();
|
||||||
|
expect(btn.classList.contains("open")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("positions wrap via style.top, not style.left", () => {
|
||||||
|
Tray.open();
|
||||||
|
expect(wrap.style.top).not.toBe("");
|
||||||
|
expect(wrap.style.left).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── close() in landscape ────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("close()", () => {
|
||||||
|
beforeEach(() => Tray.open());
|
||||||
|
|
||||||
|
it("closes the tray (display not toggled in landscape)", () => {
|
||||||
|
Tray.close();
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes .open from #id_tray_btn", () => {
|
||||||
|
Tray.close();
|
||||||
|
expect(btn.classList.contains("open")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closed top is less than open top (wrap slides up to close)", () => {
|
||||||
|
const openTop = parseInt(wrap.style.top, 10);
|
||||||
|
Tray.close();
|
||||||
|
const closedTop = parseInt(wrap.style.top, 10);
|
||||||
|
expect(closedTop).toBeLessThan(openTop);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds .snap to wrap after top transition completes", () => {
|
||||||
|
Tray.close();
|
||||||
|
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "top" }));
|
||||||
|
expect(wrap.classList.contains("snap")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes .snap from wrap once animationend fires", () => {
|
||||||
|
Tray.close();
|
||||||
|
wrap.dispatchEvent(new TransitionEvent("transitionend", { propertyName: "top" }));
|
||||||
|
wrap.dispatchEvent(new Event("animationend"));
|
||||||
|
expect(wrap.classList.contains("snap")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── drag — Y axis ──────────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("drag interaction", () => {
|
||||||
|
it("dragging down opens the tray", () => {
|
||||||
|
simulateDragY(100);
|
||||||
|
expect(Tray.isOpen()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dragging up does not open the tray", () => {
|
||||||
|
simulateDragY(-100);
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drag > 10px downward suppresses subsequent click", () => {
|
||||||
|
simulateDragY(100);
|
||||||
|
btn.click(); // should be swallowed — tray stays open
|
||||||
|
expect(Tray.isOpen()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not set style.left (Y axis only)", () => {
|
||||||
|
simulateDragY(100);
|
||||||
|
expect(wrap.style.left).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not add .wobble during drag", () => {
|
||||||
|
simulateDragY(100);
|
||||||
|
expect(wrap.classList.contains("wobble")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── click when closed — wobble, no open ───────────────────────── //
|
||||||
|
|
||||||
|
describe("clicking btn when closed", () => {
|
||||||
|
it("adds .wobble to wrap", () => {
|
||||||
|
btn.click();
|
||||||
|
expect(wrap.classList.contains("wobble")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not open the tray", () => {
|
||||||
|
btn.click();
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── click when open — close ────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("clicking btn when open", () => {
|
||||||
|
beforeEach(() => Tray.open());
|
||||||
|
|
||||||
|
it("closes the tray", () => {
|
||||||
|
btn.click();
|
||||||
|
expect(Tray.isOpen()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── init positions wrap at closed (top) ────────────────────────── //
|
||||||
|
|
||||||
|
it("init sets wrap to closed position (top < 0 or = maxTop)", () => {
|
||||||
|
// After landscape init with no real elements, _maxTop = -(wrapH_fallback - handleH_fallback)
|
||||||
|
// which will be negative. Wrap starts off-screen above.
|
||||||
|
const top = parseInt(wrap.style.top, 10);
|
||||||
|
expect(top).toBeLessThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user