Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
38 changes: 36 additions & 2 deletions src/primitives/Lazy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as s from 'solid-js';
import * as lng from '@lightningtv/solid';
import * as lngp from '@lightningtv/solid/primitives';
import * as s from 'solid-js';

type LazyProps<T extends readonly any[]> = lng.NewOmit<lng.NodeProps, 'children'> & {
each: T | undefined | null | false;
Expand All @@ -27,7 +27,13 @@ function createLazy<T>(
return props.buffer;
}
const scroll = props.scroll || props.style?.scroll;
if (!scroll || scroll === 'auto' || scroll === 'always') return props.upCount + 1;
if (
!scroll ||
scroll === 'auto' ||
scroll === 'always' ||
scroll === 'bounded'
)
return props.upCount + 1;
if (scroll === 'center') return Math.ceil(props.upCount / 2) + 1;
return 2;
});
Expand Down Expand Up @@ -64,6 +70,32 @@ function createLazy<T>(
queueMicrotask(() => viewRef.scrollToIndex(index));
}

function isInNonScrollableZone(
this: lngp.NavigableElement,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be slightly different? Can we make this one function and put it in withScrolling?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I adjusted it here.

element?: lng.ElementNode,
): boolean {
if (!viewRef) {
return false;
}
const scroll = props.scroll || viewRef.scroll;
if (scroll !== 'bounded') {
return false;
}
const upCount = props.upCount;
const totalItems = props.each ? props.each.length : 0;
if (totalItems === 0) {
return false;
}
const nonScrollableZoneStart = Math.max(0, totalItems - upCount);
if (element) {
const elementIndex = viewRef.children.indexOf(element);
if (elementIndex === -1) return false;
return elementIndex >= nonScrollableZoneStart;
}
const selected = viewRef.selected ?? 0;
return selected >= nonScrollableZoneStart;
}

const updateOffset = (_event: KeyboardEvent, container: lng.ElementNode) => {
const maxOffset = props.each ? props.each.length : 0;
const selected = container.selected || 0;
Expand Down Expand Up @@ -93,8 +125,10 @@ function createLazy<T>(
<lng.Dynamic
{...props}
component={component}
upCount={props.upCount}
{/* @once */ ...handler}
lazyScrollToIndex={lazyScrollToIndex}
isInNonScrollableZone={isInNonScrollableZone}
ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)} >
<s.Index each={items()} children={props.children} />
</lng.Dynamic>
Expand Down
30 changes: 27 additions & 3 deletions src/primitives/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,29 @@ function scrollToIndex(this: ElementNode, index: number) {
this.children[index]?.setFocus();
}

function isInNonScrollableZone(
this: ElementNode,
element?: ElementNode,
): boolean {
const scroll = this.scroll;
if (scroll !== 'bounded') {
return false;
}

const upCount = (this.upCount || 6) as number;
const totalItems = this.children.length;
const nonScrollableZoneStart = Math.max(0, totalItems - upCount);

if (element) {
const elementIndex = this.children.indexOf(element);
if (elementIndex === -1) return false;
return elementIndex >= nonScrollableZoneStart;
}

const selected = this.selected ?? 0;
return selected >= nonScrollableZoneStart;
}

const onLeft = handleNavigation('left');
const onRight = handleNavigation('right');

Expand All @@ -37,15 +60,16 @@ export const Row: Component<RowProps> = (props) => {
onRight={/* @once */ chainFunctions(props.onRight, onRight)}
forwardFocus={navigableForwardFocus}
scrollToIndex={scrollToIndex}
isInNonScrollableZone={isInNonScrollableZone}
onLayout={
/* @once */
props.selected ? chainFunctions(props.onLayout, scrollRow) : props.onLayout
}
onSelectedChanged={
/* @once */ chainFunctions(
props.onSelectedChanged,
props.scroll !== 'none' ? scrollRow : undefined,
)
props.onSelectedChanged,
props.scroll !== 'none' ? scrollRow : undefined,
)
}
style={/* @once */ combineStyles(props.style, RowStyles)}
/>
Expand Down
36 changes: 33 additions & 3 deletions src/primitives/utils/withScrolling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ export type Scroller = (
// Adds properties expected by withScrolling
export interface ScrollableElement extends ElementNode {
scrollIndex?: number;
scroll?: 'always' | 'none' | 'edge' | 'auto' | 'center';
scroll?: 'always' | 'none' | 'edge' | 'auto' | 'center' | 'bounded';
selected: number;
offset?: number;
endOffset?: number;
upCount?: number;
onScrolled?: (
elm: ScrollableElement,
offset: number,
Expand Down Expand Up @@ -130,11 +131,41 @@ export function withScrolling(isRow: boolean): Scroller {
// Default nextPosition to align with the selected position and offset
let nextPosition = rootPosition;

const totalItems = componentRef.children.length;
const upCount = componentRef.upCount || 6;
const nonScrollableZoneStart = Math.max(0, totalItems - upCount);

// Update nextPosition based on scroll type and specific conditions
if (selectedElement.centerScroll) {
nextPosition = -selectedPosition + (screenSize - selectedSizeScaled) / 2;
} else if (scroll === 'always') {
nextPosition = -selectedPosition + offset;
} else if (scroll === 'bounded') {
const isInNonScrollableZone = selected >= nonScrollableZoneStart;
const isFirstOfNonScrollableZone = selected === nonScrollableZoneStart;
const isEnteringZone =
isFirstOfNonScrollableZone &&
lastSelected !== undefined &&
lastSelected < nonScrollableZoneStart;

if (!isInNonScrollableZone) {
nextPosition = -selectedPosition + offset;
} else if (isIncrementing) {
if (isEnteringZone) {
const firstOfZoneElement =
componentRef.children[nonScrollableZoneStart];
const firstOfZonePosition = firstOfZoneElement?.[axis] ?? 0;
nextPosition = firstOfZoneElement
? -firstOfZonePosition + offset
: rootPosition;
} else {
nextPosition = rootPosition;
}
} else if (isFirstOfNonScrollableZone) {
nextPosition = -selectedPosition + offset;
} else {
nextPosition = rootPosition;
}
} else if (scroll === 'center') {
const centerPosition =
-selectedPosition +
Expand All @@ -160,7 +191,6 @@ export function withScrolling(isRow: boolean): Scroller {
nextPosition = rootPosition + selectedSize + gap;
}
} else if (isIncrementing) {
//nextPosition = -selectedPosition + offset;
nextPosition = rootPosition - selectedSize - gap;
} else {
nextPosition = rootPosition + selectedSize + gap;
Expand All @@ -174,7 +204,7 @@ export function withScrolling(isRow: boolean): Scroller {

// Prevent container from moving beyond bounds
nextPosition =
isIncrementing && scroll !== 'always'
isIncrementing && scroll !== 'always' && scroll !== 'bounded'
? Math.max(nextPosition, maxOffset)
: Math.min(nextPosition, offset);

Expand Down