From 6ee5d38f102fcfbed4eacb0d1c90915e3280ec6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andy=28=EA=B9=80=EA=B7=9C=ED=9A=8C=29?= <48755156+KimKyuHoi@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:00:53 +0900 Subject: [PATCH] fix(UI): sync seek position with hover timestamp using consistent position calculation (#9818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Previously, click/drag in the seek bar relied on the browser's default range entry event. The hover preview used 'getValueFromPosition()' separately. This resulted in a mismatch between the hover timestamp and the actual search position, This problem was particularly noticeable in long videos. ## Fix When the mouse is down, 'e.Use preventDefault()` to block the browser's default calculation, 'setBarValueForMouse_()' which follows the same pattern as the existing 'setBarValueForTouch_()' Add a function to directly call 'getValueFromPosition()' when clicking and dragging I did, now hover, click, drag, touch all go through the same function, so timestamp The preview and navigation positions are always synchronized. In addition, the hard-coded 'thumbWide = 12' is 'thumbRadius = 6' ('range_elements.less') I modified it to the defined '@thumb-size') and the browser changed the thumb [[rect.left + thumbRadius, rect.right - thumbRadius]]' only within the range of '[rect.left + thumbRadius]', Reflected the offset value when the click position was reverse-calculated as a value. --------- Co-authored-by: Claude Code Co-authored-by: Álvaro Velad Galván --- ui/range_element.js | 72 ++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/ui/range_element.js b/ui/range_element.js index 1f1c3b74d..753da63da 100644 --- a/ui/range_element.js +++ b/ui/range_element.js @@ -48,6 +48,9 @@ shaka.ui.RangeElement = class extends shaka.ui.Element { /** @private {boolean} */ this.isChanging_ = false; + /** @private {boolean} */ + this.isMouseChanging_ = false; + /** @protected {!HTMLInputElement} */ this.bar = /** @type {!HTMLInputElement} */ (document.createElement('input')); @@ -74,23 +77,48 @@ shaka.ui.RangeElement = class extends shaka.ui.Element { this.bar.disabled = false; }); - this.eventManager.listen(this.controls, 'showingui', (e) => { + this.eventManager.listen(this.controls, 'showingui', () => { this.showingUITimer_.tickAfter(/* seconds= */ 0); }); - this.eventManager.listen(this.controls, 'hidingui', (e) => { + this.eventManager.listen(this.controls, 'hidingui', () => { this.showingUITimer_.stop(); this.bar.disabled = true; }); this.eventManager.listen(this.bar, 'mousedown', (e) => { if (!this.bar.disabled) { + // Prevent native range update to use getValueFromPosition() + // consistently with the hover preview. + e.preventDefault(); + this.bar.focus(); this.isChanging_ = true; + this.isMouseChanging_ = true; + this.setBarValueForMouse_(e); this.onChangeStart(); + this.onChange(); e.stopPropagation(); } }); + this.eventManager.listen(document, 'mousemove', (e) => { + if (this.isMouseChanging_) { + this.setBarValueForMouse_(e); + this.onChange(); + } + }); + + this.eventManager.listen(document, 'mouseup', (e) => { + if (this.isMouseChanging_) { + this.isMouseChanging_ = false; + if (this.isChanging_) { + this.isChanging_ = false; + this.setBarValueForMouse_(e); + this.onChangeEnd(); + } + } + }); + this.eventManager.listen(this.bar, 'touchstart', (e) => { if (!this.bar.disabled) { this.isChanging_ = true; @@ -133,6 +161,8 @@ shaka.ui.RangeElement = class extends shaka.ui.Element { this.eventManager.listen(this.bar, 'mouseup', (e) => { if (this.isChanging_) { this.isChanging_ = false; + this.isMouseChanging_ = false; + this.setBarValueForMouse_(e); this.onChangeEnd(); e.stopPropagation(); } @@ -141,6 +171,7 @@ shaka.ui.RangeElement = class extends shaka.ui.Element { this.eventManager.listen(this.bar, 'blur', () => { if (this.isChanging_) { this.isChanging_ = false; + this.isMouseChanging_ = false; this.onChangeEnd(); } }); @@ -250,39 +281,40 @@ shaka.ui.RangeElement = class extends shaka.ui.Element { * @return {number} */ getValueFromPosition(clientX) { - // Get the bounding rectangle of the range input element const rect = this.bar.getBoundingClientRect(); - - // Parse the min, max, and step attributes from the input element const min = parseFloat(this.bar.min); const max = parseFloat(this.bar.max); const step = parseFloat(this.bar.step) || 1; - // Define the effective range of the thumb movement - // 12 is the value of @thumb-size in range_elements.less. Note: for - // everything to work, this value has to be synchronized. - const thumbWidth = 12; - const minX = rect.left + thumbWidth / 2; - const maxX = rect.right - thumbWidth / 2; - - // Clamp the touch X position to stay within the thumb's movement range + // thumbRadius is half of @thumb-size, as defined in range_elements.less. + // The browser renders the thumb only within the movement range + // [rect.left + thumbRadius, rect.right - thumbRadius], so we must apply + // the same offset when mapping a click position back to a value. + // Note: thumbRadius must stay in sync with @thumb-size in + // range_elements.less. + const thumbRadius = 6; // half of @thumb-size in range_elements.less + const minX = rect.left + thumbRadius; + const maxX = rect.right - thumbRadius; const clampedX = Math.max(minX, Math.min(maxX, clientX)); - - // Calculate the percentage of the track that the clamped X represents const percent = (clampedX - minX) / (maxX - minX); - // Convert the percentage into a value within the input's range let value = min + percent * (max - min); - - // Round the value to the nearest step value = Math.round((value - min) / step) * step + min; - - // Ensure the value stays within the min and max bounds value = Math.min(max, Math.max(min, value)); return value; } + /** + * Synchronize the mouse position with the range value. + * @param {Event} event + * @private + */ + setBarValueForMouse_(event) { + this.bar.value = this.getValueFromPosition( + /** @type {MouseEvent} */ (event).clientX); + } + /** * Synchronize the touch position with the range value. * Comes in handy on iOS, where users have to grab the handle in order