Skip to content
This repository has been archived by the owner on Apr 18, 2024. It is now read-only.

fix: LSDV-5256: Stop automatically scrolling if current Paragraph is not on the screen #1502

Merged
merged 21 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ee8516a
fix: LSDV-5256: Stop automatically scrolling if current Paragraph is …
juliosgarbi Jul 16, 2023
578b8f8
remove leftover console log
juliosgarbi Jul 16, 2023
bc154b3
start get the padding element and stop using magic numbers
juliosgarbi Jul 17, 2023
6f876ff
remove leftover console log
juliosgarbi Jul 17, 2023
e27acc6
fix audio seek sync with scroll
juliosgarbi Jul 20, 2023
ace2f80
small fixies about scroll position and scroll validation
juliosgarbi Jul 21, 2023
75bb1be
fix regression with auto scroll if Phrase is annotated
juliosgarbi Jul 24, 2023
e458e5a
fix the auto scroll position
juliosgarbi Jul 24, 2023
d5883dc
merge conflcits
juliosgarbi Jul 24, 2023
180e500
remove unecessary comment
juliosgarbi Jul 24, 2023
3c07f88
changing the scroll position to stop breaking the UI
juliosgarbi Jul 24, 2023
3c8e1f2
roll back the changes
juliosgarbi Jul 24, 2023
6dd1832
fix the way that the reading line is rendering to influence the Parag…
juliosgarbi Jul 24, 2023
52103a4
removing the double FF validation
juliosgarbi Jul 24, 2023
c4c042e
trigger the auto scroll when user changes the author filter
juliosgarbi Jul 25, 2023
8ef04aa
change the timeout delay to get automatically from css
juliosgarbi Jul 25, 2023
778d60c
fix the overlap timing
juliosgarbi Jul 25, 2023
16fe11f
fix playingId calc and reuse intersection observer
bmartel Jul 26, 2023
dd30aac
fix tests
juliosgarbi Jul 26, 2023
995ea52
revert observer change
juliosgarbi Jul 27, 2023
d9b4888
fix tests
juliosgarbi Jul 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions e2e/tests/sync/audio-paragraphs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,5 +352,56 @@ FFlagMatrix(['fflag_feat_front_lsdv_e_278_contextual_scrolling_short'], function

await assert.equal(scrollPosition.scrollTop, 0);
});

FFlagScenario('Paragraph shouldnt automatically scroll if user manually scroll and the current paragraph is not in the screen', async function({ I, LabelStudio, AtAudioView }) {
LabelStudio.setFeatureFlags({
ff_front_dev_2715_audio_3_280722_short: true,
ff_front_1170_outliner_030222_short: true,
...flags,
});

params.config = configWithScroll;

I.amOnPage('/');

LabelStudio.init(params);

await AtAudioView.waitForAudio();
await AtAudioView.lookForStage();

const [{ currentTime: startingAudioTime }, { currentTime: startingParagraphAudioTime }] = await AtAudioView.getCurrentAudio();

assert.equal(startingAudioTime, startingParagraphAudioTime);
assert.equal(startingParagraphAudioTime, 0);

AtAudioView.clickPlayButton();

I.wait(2);

I.executeScript( () => {
document.querySelector('[data-testid="phrases-wrapper"]').scrollTo(0, 1000);

const wheelEvt = document.createEvent('MouseEvents');

wheelEvt.initEvent('wheel', true, true);

wheelEvt.deltaY = 1200;

document.querySelector('[data-testid="phrases-wrapper"]').dispatchEvent(wheelEvt);
});

I.wait(5);

const scrollPosition = await I.executeScript(function(selector) {
const element = document.querySelector(selector);

return {
scrollTop: element.scrollTop,
scrollLeft: element.scrollLeft,
};
}, '[data-testid="phrases-wrapper"]');

await assert(scrollPosition.scrollTop > 400, 'Scroll position should be greater than 200');
});
}
});
60 changes: 37 additions & 23 deletions src/tags/object/Paragraphs/HtxParagraphs.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class HtxParagraphsView extends Component {
this.isPlaying = false;
this.state = {
canScroll: true,
inViewport: true,
};
}

Expand Down Expand Up @@ -417,42 +418,56 @@ class HtxParagraphsView extends Component {


if (isFF(FF_LSDV_E_278) && this.props.item.contextscroll && item.playingId >= 0 && this.lastPlayingId !== item.playingId && this.state.canScroll) {
const _padding = 8; // 8 is the padding between the phrases, so it will keep aligned with the top of the phrase
const _padding = parseInt(window.getComputedStyle(this.myRef.current)?.getPropertyValue('padding-top')) || 0;
const _playingItem = this.props.item._value[item.playingId];
const _start = _playingItem.start;
const _end = _playingItem.end;
const _phaseHeight = this.activeRef.current?.offsetHeight || 0;
const _duration = this.props.item._value[item.playingId].duration || _end - _start;
const _wrapperHeight = root.offsetHeight;
const _wrapperOffsetTop = this.activeRef.current?.offsetTop - _padding;
const _splittedText = 10; // it will be from 0 to 100% of the text height, going 10% by 10%
const _splittedText = Math.ceil(this.activeRef.current?.offsetHeight / this.myRef.current?.offsetHeight) + 1; // +1 to make sure the last line is scrolled to the top

this._disposeTimeout();

if (_phaseHeight > _wrapperHeight) {
for (let i = 0; i < _splittedText; i++) {
this.scrollTimeout.push(
setTimeout(() => {
const _pos = (_wrapperOffsetTop) + ((_phaseHeight - (_wrapperHeight / 3)) * (i * .1)); // 1/3 of the wrapper height is the offset to keep the text aligned with the middle of the wrapper
const _pos = (_wrapperOffsetTop) + ((_phaseHeight) * (i * (1 / _splittedText)));

root.scrollTo({
top: _pos,
behavior: 'smooth',
});
if (this.state.inViewPort && this.state.canScroll) {
root.scrollTo({
top: _pos,
behavior: 'smooth',
});
}
}, ((_duration / _splittedText) * i) * 1000),
);
}
} else {
root.scrollTo({
top: _wrapperOffsetTop,
behavior: 'smooth',
});
if (this.state.inViewPort) {
root.scrollTo({
top: _wrapperOffsetTop,
behavior: 'smooth',
});
}
}

this.lastPlayingId = item.playingId;
}
}

_handleScrollToPhrase() {
const _padding = parseInt(window.getComputedStyle(this.myRef.current)?.getPropertyValue('padding-top')) || 0;
const _wrapperOffsetTop = this.activeRef.current?.offsetTop - _padding;

this.myRef.current.scrollTo({
top: _wrapperOffsetTop,
behavior: 'smooth',
});
}

_handleScrollContainerHeight() {
const container = this.myRef.current;
const mainContentView = document.querySelector('.lsf-main-content');
Expand All @@ -470,10 +485,6 @@ class HtxParagraphsView extends Component {

}

_handleScrollRoot() {
this._disposeTimeout();
}

_resizeObserver = new ResizeObserver(() => this._handleScrollContainerHeight());

componentDidUpdate() {
Expand All @@ -483,21 +494,19 @@ class HtxParagraphsView extends Component {
componentDidMount() {
if(isFF(FF_LSDV_E_278) && this.props.item.contextscroll) this._resizeObserver.observe(document.querySelector('.lsf-main-content'));
this._handleUpdate();

if(isFF(FF_LSDV_E_278))
this.myRef.current.addEventListener('wheel', this._handleScrollRoot.bind(this));
}

componentWillUnmount() {
const target = document.querySelector('.lsf-main-content');

if(isFF(FF_LSDV_E_278))
this.myRef.current.removeEventListener('wheel', this._handleScrollRoot);

if (target) this._resizeObserver?.unobserve(target);
this._resizeObserver?.disconnect();
}

setIsInViewPort(isInViewPort) {
this.setState({ inViewPort: isInViewPort });
}

renderWrapperHeader() {
const { item } = this.props;

Expand All @@ -511,7 +520,12 @@ class HtxParagraphsView extends Component {
data-testid={'auto-scroll-toggle'}
checked={this.state.canScroll}
onChange={() => {
this.setState({ canScroll: !this.state.canScroll });
if (!this.state.canScroll)
this._handleScrollToPhrase();

this.setState({
canScroll: !this.state.canScroll,
});
}}
label={'Auto-scroll'}
/>
Expand Down Expand Up @@ -560,7 +574,7 @@ class HtxParagraphsView extends Component {
className={contextScroll ? styles.scroll_container : styles.container}
onMouseUp={this.onMouseUp.bind(this)}
>
<Phrases item={item} playingId={item.playingId} {...(isFF(FF_LSDV_E_278) ? { activeRef: this.activeRef }: {})} />
<Phrases setIsInViewport={this.setIsInViewPort.bind(this)} item={item} playingId={item.playingId} {...(isFF(FF_LSDV_E_278) ? { activeRef: this.activeRef }: {})} />
</div>
</ObjectTag>
);
Expand Down
13 changes: 13 additions & 0 deletions src/tags/object/Paragraphs/Paragraphs.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,17 @@ $border-thin: 1px solid rgba(137, 128, 152, 0.16);
color: #898098;
}
}

.wrapperText {
position: relative;

.readingLine {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 20px;
pointer-events: none;
}
}
}
126 changes: 123 additions & 3 deletions src/tags/object/Paragraphs/Phrases.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PauseCircleOutlined, PlayCircleOutlined } from '@ant-design/icons';
import styles from './Paragraphs.module.scss';
import { FF_LSDV_E_278, isFF } from '../../../utils/feature-flags';
import { IconPause, IconPlay } from '../../../assets/icons';
import { useCallback, useEffect, useState } from 'react';

const formatTime = (seconds) => {
if (isNaN(seconds)) return '';
Expand All @@ -20,9 +21,112 @@ const formatTime = (seconds) => {
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
};

export const Phrases = observer(({ item, playingId, activeRef }) => {
export const Phrases = observer(({ item, playingId, activeRef, setIsInViewport }) => {
const [animationKeyFrame, setAnimationKeyFrame] = useState(null);
const [seek, setSeek] = useState(0);
const [isSeek, setIsSeek] = useState(null);
const cls = item.layoutClasses;
const withAudio = !!item.audio;
let observer;

// default function to animate the reading line
const animateElement = useCallback(
(element, start, duration, isPlaying = true) => {
if (!element || !isFF(FF_LSDV_E_278)) return;

const _animationKeyFrame = element.animate(
[{ top: `${start}%` }, { top: '100%' }],
{
easing: 'linear',
duration: duration * 1000,
},
);

if (isPlaying)
_animationKeyFrame.play();
else
_animationKeyFrame.pause();

setAnimationKeyFrame(_animationKeyFrame);
},
[animationKeyFrame, setAnimationKeyFrame],
);

// this function is used to animate the reading line when user seek audio
const setSeekAnimation = useCallback(
(isSeeking) => {
if (!isFF(FF_LSDV_E_278)) return;

const duration = item._value[playingId]?.duration || item._value[playingId]?.end - item._value[playingId]?.start;
const endTime = !item._value[playingId]?.end ? item._value[playingId]?.start + item._value[playingId]?.duration : item._value[playingId]?.end;
const seekDuration = endTime - seek.time;
const startValue = 100 - ((seekDuration * 100) / duration);

if (startValue > 0 && startValue < 100)
animateElement(activeRef.current?.querySelector('.reading-line'), startValue, seekDuration, seek.playing);
else
setIsSeek(isSeeking);
},
[seek, playingId],
);

// useRef to get the reading line element
const readingLineRef = useCallback(node => {
if(observer) {
observer.disconnect();
}

if(node !== null) {
const duration = item._value[playingId]?.duration || item._value[playingId]?.end - item._value[playingId]?.start;

if (!isNaN(duration)) {
animateElement(node, 0, duration, item.playing);
}

observer = new IntersectionObserver((entries) => {
setIsInViewport(entries[0].isIntersecting);
}, {
rootMargin: '0px',
});

observer.observe(node);
}
}, [playingId]);

useEffect(() => {
if (!isFF(FF_LSDV_E_278)) return;

item.syncHandlers.set('seek', seek => {
item.handleSyncPlay(seek);
setSeek(seek);
setIsInViewport(true);
});

return () => {
observer?.disconnect();
};
}, []);


// when user seek audio, the useEffect will be triggered and animate the reading line to the seek position
useEffect(() => {
setSeekAnimation(true);
}, [seek]);

// when user seek audio to a different playing phrase, the useEffect will be triggered and animate the reading line to the seek position
useEffect(() => {
if (!isSeek) return;
bmartel marked this conversation as resolved.
Show resolved Hide resolved

setSeekAnimation(false);
}, [playingId]);

// when user click on play/pause button, the useEffect will be triggered and pause or play the reading line animation
useEffect(() => {
if (!isFF(FF_LSDV_E_278)) return;

if (item.playing) animationKeyFrame?.play();
else animationKeyFrame?.pause();
}, [item.playing]);

if (!item._value) return null;
const val = item._value.map((v, idx) => {
Expand Down Expand Up @@ -56,7 +160,10 @@ export const Phrases = observer(({ item, playingId, activeRef }) => {
isFF(FF_LSDV_E_278) ?
<IconPlay /> : <PlayCircleOutlined />
}
onClick={() => item.play(idx)}
onClick={() => {
setIsInViewport(true);
item.play(idx);
}}
/>
)}
{isFF(FF_LSDV_E_278) ? (
Expand All @@ -68,7 +175,20 @@ export const Phrases = observer(({ item, playingId, activeRef }) => {
<span className={cls?.name} data-skip-node="true" style={style?.name}>{v[item.namekey]}</span>
)}

<span className={cls?.text}>{v[item.textkey]}</span>
{isFF(FF_LSDV_E_278) ? (
<span className={styles.wrapperText}>
{(isActive) && (
<span ref={readingLineRef} className={`${styles.readingLine} reading-line`} data-skip-node="true"></span>
)}
<span className={`${cls?.text}`}>
{v[item.textkey]}
</span>
</span>
) : (
<span className={`${cls?.text}`}>
{v[item.textkey]}
</span>
)}
</div>
);
});
Expand Down