Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@lightningjs/ui-components

Package Overview
Dependencies
Maintainers
7
Versions
95
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lightningjs/ui-components - npm Package Compare versions

Comparing version 1.3.0-beta.1 to 1.3.0

elements/Base/Base.test.js

4

elements/Keyboard/index.js

@@ -135,3 +135,3 @@ /**

keyboard.selectedIndex = row;
keyboard.Items.children[row].selectedIndex = column;
keyboard._Items.children[row].selectedIndex = column;
}

@@ -149,3 +149,3 @@ }

row = keyboard.selectedIndex;
column = keyboard.Items.children[row].selectedIndex;
column = keyboard._Items.children[row].selectedIndex;
}

@@ -152,0 +152,0 @@ return { row, column };

@@ -18,3 +18,2 @@ /**

*/
export * from './elements';

@@ -21,0 +20,0 @@ export * from './layout';

/**
* Copyright 2021 Comcast Cable Communications Management, LLC
* Copyright 2022 Comcast Cable Communications Management, LLC
*

@@ -18,3 +18,2 @@ * Licensed under the Apache License, Version 2.0 (the "License");

*/
import lng from '@lightningjs/core';

@@ -224,2 +223,3 @@ import Column from '.';

type: Column,
h: 500,
itemSpacing: args.itemSpacing,

@@ -249,2 +249,3 @@ items: Array.apply(null, { length: 15 }).map(() => ({

plinko: true,
h: 500,
items: Array.apply(null, { length: 15 }).map((_, i) => ({

@@ -603,1 +604,177 @@ type: ExpandingRow,

};
export const LazyUpCount = args =>
class LazyUpCount extends lng.Component {
static _template() {
return {
Column: {
type: Column,
h: 500,
itemSpacing: args.itemSpacing,
scrollIndex: args.scrollIndex,
lazyUpCount: args.lazyUpCount,
items: Array.apply(null, { length: 20 }).map((_, i) => ({
type: Button,
buttonText: `Button ${i + 1}`
})),
alwaysScroll: args.alwaysScroll
}
};
}
_getFocused() {
return this.tag('Column');
}
};
LazyUpCount.args = {
scrollIndex: 0,
lazyUpCount: 5,
itemTransition: 0.4
};
LazyUpCount.argTypes = {
itemTransition: {
control: { type: 'number', min: 0, step: 0.1 }
},
scroll: {
control: { type: 'select', options: [1, 5, 15, 20] }
},
scrollIndex: {
control: { type: 'number', min: 0 }
},
lazyUpCount: {
control: { type: 'number', min: 0 }
},
alwaysScroll: {
control: { type: 'boolean' }
}
};
LazyUpCount.parameters = {
argActions: {
scroll: function(index, component) {
component.tag('Column').scrollTo(index - 1);
},
itemTransition: (duration, component) => {
component.tag('Column').itemTransition = {
duration,
timingFunction: component.tag('Column')._itemTransition.timingFunction
};
}
}
};
export const AddingItems = args =>
class AddingItems extends lng.Component {
static _template() {
return {
Column: {
type: Column,
h: 500,
itemSpacing: args.itemSpacing,
scrollIndex: args.scrollIndex,
items: Array.apply(null, { length: 20 }).map((_, i) => ({
type: Button,
buttonText: `Button ${i + 1}`
}))
}
};
}
_init() {
super._init();
setTimeout(() => {
this.tag('Column').appendItemsAt(
[
{
type: Button,
buttonText: 'New Button 0'
},
{
type: Button,
buttonText: 'New Button 1'
},
{
type: Button,
buttonText: 'New Button 2'
}
],
3
);
}, 3000);
setTimeout(() => {
this.tag('Column').prependItems([
{
type: Button,
buttonText: 'New Button 3',
w: 150
},
{
type: Button,
buttonText: 'New Button 4',
w: 150
},
{
type: Button,
buttonText: 'New Button 5',
w: 150
}
]);
}, 3750);
}
_getFocused() {
return this.tag('Column');
}
};
AddingItems.args = {
itemSpacing: 20,
scrollIndex: 0
};
AddingItems.argTypes = {
itemSpacing: {
control: { type: 'range', min: 0, max: 100, step: 5 }
},
scrollIndex: {
control: 'number'
}
};
export const RemovingItems = args =>
class RemovingItems extends lng.Component {
static _template() {
return {
Column: {
type: Column,
h: 500,
itemSpacing: args.itemSpacing,
scrollIndex: args.scrollIndex,
items: Array.apply(null, { length: 20 }).map((_, i) => ({
type: Button,
buttonText: `Button ${i + 1}`
}))
}
};
}
_init() {
super._init();
setTimeout(() => {
this.tag('Column').removeItemAt(1);
}, 3000);
}
_getFocused() {
return this.tag('Column');
}
};
RemovingItems.args = {
itemSpacing: 20,
scrollIndex: 0
};
RemovingItems.argTypes = {
itemSpacing: {
control: { type: 'range', min: 0, max: 100, step: 5 }
},
scrollIndex: {
control: 'number'
}
};
/**
* Copyright 2021 Comcast Cable Communications Management, LLC
* Copyright 2022 Comcast Cable Communications Management, LLC
*

@@ -18,5 +18,4 @@ * Licensed under the Apache License, Version 2.0 (the "License");

*/
import FocusManager from '../FocusManager';
import { getY, getW } from '../../utils';
import { getY, getW, delayForAnimation } from '../../utils';
import { debounce } from 'debounce';

@@ -31,2 +30,14 @@ export default class Column extends FocusManager {

static get properties() {
return [
...super.properties,
'itemSpacing',
'scrollIndex',
'alwaysScroll',
'lazyUpCount',
'neverScroll',
'autoResize'
];
}
_construct() {

@@ -36,15 +47,4 @@ super._construct();

this._itemSpacing = 0;
this._itemPosX = 0;
this._itemPosY = 0;
this._scrollIndex = 0;
this._whenEnabled = new Promise(resolve => (this._firstEnable = resolve));
this.debounceDelay = Number.isInteger(this.debounceDelay)
? this.debounceDelay
: 30;
this._update = debounce(this._updateLayout, this.debounceDelay);
this._updateImmediate = debounce(
this._updateLayout,
this.debounceDelay,
true
);
this._performRenderDebounce = debounce(this._performRender.bind(this), 0);
}

@@ -55,12 +55,36 @@

if (!this.h) {
// if height is undefinend or 0, set the Columns's height
this.h =
this.parent && // if the Column is a child item in a FocusManager (like Row)
// if height is undefined or 0, set the Columns's height
if (
// if the Column is a child item in a FocusManager (like Row)
this.parent &&
this.parent.parent &&
this.parent.parent instanceof FocusManager
? this.parent.parent.h
: this.stage.h;
this.parent.parent instanceof FocusManager &&
this.parent.parent.h
) {
this.h = this.parent.parent.h;
} else {
let parent = this.p;
while (parent && !parent.h) {
parent = parent.p;
}
this.h =
(parent && parent.h) ||
this.stage.h / this.stage.getRenderPrecision();
}
}
this.Items.transition('y').on(
'finish',
this._transitionListener.bind(this)
);
}
_update() {
this._updateLayout();
}
_setItemSpacing(itemSpacing) {
return itemSpacing || 0;
}
get _itemTransition() {

@@ -85,2 +109,7 @@ return (

this._smooth = true;
if (this._lazyItems && this._lazyItems.length) {
delayForAnimation(() => {
this.appendItems(this._lazyItems.splice(0, 1));
});
}
return super.selectNext();

@@ -129,4 +158,3 @@ }

// end of Items container < end of last item
Math.abs(this._itemsY - this.h) <
lastChild.y + this.Items.childList.last.h
Math.abs(this.itemPosY - this.h) < lastChild.y + lastChild.h
);

@@ -150,3 +178,3 @@ }

this._performRender();
this._performRenderDebounce();
}

@@ -201,2 +229,3 @@

this.Items.y = this.itemPosY;
this._updateTransitionTarget(this.Items, 'y', this.itemPosY);
}

@@ -224,5 +253,8 @@ } else if (this._shouldScroll()) {

} else {
const scrollTarget =
-scrollItem.y + (scrollItem === this.selected ? scrollOffset : 0);
this.Items.patch({
y: -scrollItem.y + (scrollItem === this.selected ? scrollOffset : 0)
y: scrollTarget
});
this._updateTransitionTarget(this.Items, 'y', scrollTarget);
}

@@ -235,43 +267,2 @@ }

get onScreenItems() {
return this.Items.children.filter(child => this._isOnScreen(child));
}
_isOnScreen(child) {
if (!child) return false;
const y = getY(child);
if (!Number.isFinite(y)) return false;
// to calculate the target absolute Y position of the item, we need to use
// 1) the entire column's absolute position,
// 2) the target animation value of the items container, and
// 3) the target value of the item itself
const ItemY =
this.core.renderContext.py + this.Items.transition('y').targetValue + y;
const { h } = child;
// check that the child is inside the bounds of the stage
const withinTopStageBounds = ItemY + h > 0;
// stage height needs to be adjusted with precision since all other values assume the original height and width (pre-scaling)
const withinBottomStageBounds =
ItemY < this.stage.h / this.stage.getRenderPrecision();
// check that the child is inside the bounds of any clipping
let withinTopClippingBounds = true;
let withinBottomClippingBounds = true;
if (this.core._scissor && this.core._scissor.length) {
// _scissor consists of [ left position (x), top position (y), width, height ]
const topBounds = this.core._scissor[1];
const bottomBounds = topBounds + this.core._scissor[3];
withinTopClippingBounds = Math.round(ItemY + h) > Math.round(topBounds);
withinBottomClippingBounds = Math.round(ItemY) < Math.round(bottomBounds);
}
return (
withinTopStageBounds &&
withinBottomStageBounds &&
withinTopClippingBounds &&
withinBottomClippingBounds
);
}
_updateLayout() {

@@ -289,6 +280,8 @@ this._whenEnabled.then(() => {

child.patch({ y: nextY });
this._updateTransitionTarget(child, 'y', nextY);
}
nextY += child.h;
if (i < this.Items.children.length - 1) {
nextY += this.itemSpacing;
const extraItemSpacing = child.extraItemSpacing || 0;
nextY += this.itemSpacing + extraItemSpacing;
}

@@ -305,4 +298,11 @@

}
const itemChanged = this.Items.w !== nextW || this.Items.h !== nextY;
this.Items.patch({ w: nextW, h: nextY });
if (this.autoResize) {
this.h = this.Items.h;
this.w = this.Items.w;
}
const lastChild = this.Items.childList.last;

@@ -330,60 +330,68 @@ const endOfLastChild = lastChild ? getY(lastChild) + lastChild.h : 0;

this._performRender();
itemChanged && this.fireAncestors('$itemChanged');
this._performRenderDebounce();
});
}
get itemSpacing() {
return this._itemSpacing;
get _itemsY() {
return getY(this.Items);
}
set itemSpacing(itemSpacing) {
if (itemSpacing !== this._itemSpacing) {
this._itemSpacing = itemSpacing;
this._update();
appendItems(items = []) {
const itemWidth = this.renderWidth;
this._smooth = false;
if (items.length > this.lazyUpCount + 2) {
this._lazyItems = items.splice(this.lazyUpCount + 2);
}
}
get scrollIndex() {
return this._scrollIndex;
items.forEach(item => {
item.parentFocus = this.hasFocus();
item = this.Items.childList.a(item);
item.w = getW(item) || itemWidth;
});
this.stage.update();
this._update();
this._refocus();
}
set scrollIndex(scrollIndex) {
if (scrollIndex !== this._scrollIndex) {
this._scrollIndex = scrollIndex;
this._update();
}
}
appendItemsAt(items = [], idx) {
const addIndex = Number.isInteger(idx) ? idx : this.Items.children.length;
this._smooth = false;
this._lastAppendedIdx = addIndex;
set itemPosX(x) {
this.Items.x = this._itemPosX = x;
}
items.forEach((item, itemIdx) => {
this.Items.childList.addAt(
{
...item,
parentFocus: this.hasFocus()
},
addIndex + itemIdx
);
});
get itemPosX() {
return this._itemPosX;
}
if (this.selectedIndex >= this._lastAppendedIdx) {
this._selectedIndex += items.length;
}
set itemPosY(y) {
this.Items.y = this._itemPosY = y;
this._update();
this._refocus();
}
get itemPosY() {
return this._itemPosY;
prependItems(items) {
this.appendItemsAt(items, 0);
}
get _itemsY() {
return getY(this.Items);
}
appendItems(items = []) {
const itemWidth = this.renderWidth;
removeItemAt(index) {
this._smooth = false;
this.Items.childList.removeAt(index);
items.forEach(item => {
item.parentFocus = this.hasFocus();
item = this.Items.childList.a(item);
item.w = getW(item) || itemWidth;
});
this.stage.update();
this._updateLayout();
this._update.clear();
if (
this.selectedIndex > index ||
this.selectedIndex === this.Items.children.length
) {
this._selectedIndex--;
}
this._update();
this._refocus();

@@ -403,7 +411,11 @@ }

}
this.Items.transition('y').on('finish', () => (this._smooth = false));
}
_transitionListener() {
this._smooth = false;
this.transitionDone();
}
$itemChanged() {
this._updateImmediate();
this._update();
}

@@ -415,5 +427,6 @@

this.Items.childList.remove(item);
this._updateImmediate();
this._update();
if (wasSelected || this.selectedIndex >= this.items.length) {
// eslint-disable-next-line no-self-assign
this.selectedIndex = this._selectedIndex;

@@ -429,7 +442,16 @@ }

$columnChanged() {
this._updateImmediate();
this._update();
}
_isOnScreen(child) {
if (!child) return false;
return this._isComponentVerticallyVisible(child);
}
// can be overridden
onScreenEffect() {}
// can be overridden
transitionDone() {}
}
/**
* Copyright 2021 Comcast Cable Communications Management, LLC
* Copyright 2022 Comcast Cable Communications Management, LLC
*

@@ -18,3 +18,2 @@ * Licensed under the Apache License, Version 2.0 (the "License");

*/
import lng from '@lightningjs/core';

@@ -21,0 +20,0 @@ import FocusManager from '.';

/**
* Copyright 2021 Comcast Cable Communications Management, LLC
* Copyright 2022 Comcast Cable Communications Management, LLC
*

@@ -23,21 +23,34 @@ * Licensed under the Apache License, Version 2.0 (the "License");

*/
import Base from '../../elements/Base';
import { getX, getY, isComponentOnScreen } from '../../utils';
import lng from '@lightningjs/core';
export default class FocusManager extends Base {
static get tags() {
return ['Items'];
}
export default class FocusManager extends lng.Component {
static _template() {
return { Items: {} };
static get properties() {
return ['direction'];
}
_construct() {
super._construct();
this._selectedIndex = 0;
this._itemPosX = 0;
this._itemPosY = 0;
this.direction = this.direction || 'row';
}
get direction() {
return this._direction;
_init() {
this._checkSkipFocus();
}
set direction(direction) {
this._direction = direction;
get Items() {
if (!this.tag('Items')) {
this.patch({ Items: {} });
}
return this._Items;
}
_setDirection(direction) {
const state = {

@@ -52,6 +65,7 @@ none: 'None',

}
return direction;
}
get Items() {
return this.tag('Items');
_getItems() {
return this._Items.children;
}

@@ -64,5 +78,40 @@

set items(items) {
this.Items.childList.clear();
this._resetItems();
this._selectedIndex = 0;
this.appendItems(items);
this._checkSkipFocus();
}
set itemPosX(x) {
this.Items.x = this._itemPosX = x;
}
get itemPosX() {
return this._itemPosX;
}
set itemPosY(y) {
this.Items.y = this._itemPosY = y;
}
get itemPosY() {
return this._itemPosY;
}
_resetItems() {
this.Items.childList.clear();
this.Items.patch({
w: 0,
h: 0,
x: this.itemPosX,
y: this.itemPosY
});
}
appendItems(items = []) {
this.Items.childList.a(items);
this._refocus();
}
_checkSkipFocus() {
// If the first item has skip focus when appended get the next focusable item

@@ -75,7 +124,2 @@ const initialSelection = this.Items.children[this.selectedIndex];

appendItems(items = []) {
this.Items.childList.a(items);
this._refocus();
}
get selected() {

@@ -100,4 +144,3 @@ return this.Items.children[this.selectedIndex];

if (this.selected) {
this.render(this.selected, this.prevSelected);
this.signal('selectedChange', this.selected, this.prevSelected);
this._selectedChange(this.selected, this.prevSelected);
}

@@ -110,2 +153,7 @@ // Don't call refocus until after a new render in case of a situation like Plinko nav

_selectedChange(selected, prevSelected) {
this.render(selected, prevSelected);
this.signal('selectedChange', selected, prevSelected);
}
// Override

@@ -154,6 +202,8 @@ render() {}

this.selectedIndex = previousItemIndex;
return true;
} else if (this.wrapSelected) {
this.selectedIndex = this._lastFocusableIndex();
return true;
}
return true;
return false;
}

@@ -177,6 +227,8 @@

this.selectedIndex = nextIndex;
return true;
} else if (this.wrapSelected) {
this.selectedIndex = this._firstFocusableIndex();
return true;
}
return true;
return false;
}

@@ -209,3 +261,6 @@

.map((item, index) => {
const [x, y] = item.core.getAbsoluteCoords(0, 0);
let [x, y] = [0, 0];
if (item.core) {
[x, y] = item.core.getAbsoluteCoords(0, 0);
}
return {

@@ -227,3 +282,3 @@ index,

})
.sort(function(a, b) {
.sort(function (a, b) {
return a.distance - b.distance;

@@ -235,2 +290,9 @@ });

/**
* TODO: Update Base to remove the focus/unfocus calls and add a second "BaseComponent" that does have them
*
*/
_focus() {}
_unfocus() {}
_getFocused() {

@@ -249,2 +311,133 @@ const { selected } = this;

_updateTransitionTarget(element, property, newValue) {
if (
element &&
element.transition(property) &&
!element.transition(property).isRunning() &&
element.transition(property).targetValue !== newValue
) {
element.transition(property).updateTargetValue(newValue);
}
}
/**
* Return list of items that are currently fully and partially on screen
* @returns {Array} Array of matching lng.Component objects or empty array
*/
get onScreenItems() {
return this.Items.children.filter(child => this._isOnScreen(child));
}
_isOnScreenCompletely(child) {
// 'isFullyOnScreen' method has been added to the Base class.
// in case child does _not_ extend Base, 'isComponentOnScreen'
// from the 'util' module will be invoked. The same method is
// invoked by Base class
return child.isFullyOnScreen
? child.isFullyOnScreen()
: isComponentOnScreen(child);
}
get fullyOnScreenItems() {
return this.Items.children.reduce((rv, item) => {
if (item instanceof FocusManager) {
return [
...rv,
...item.Items.children.filter(this._isOnScreenCompletely)
];
} else if (this._isOnScreenCompletely(item)) {
return [...rv, item];
} else {
return rv;
}
}, []);
}
_isOnScreen() {
throw new Error("'_isOnScreen' must be implemented by 'row'/'column'");
}
_isComponentHorizontallyVisible(child) {
// get child's destination X; If child is moving to a destination,
// get the value of where child will end up
const x = getX(child);
if (!Number.isFinite(x)) return false;
// to calculate the target absolute X position of the item, we need to use
// 1) the entire component's absolute position,
// 2) the target animation value of the items container, and
// 3) the target value of the item itself
const transitionX = this.getTransitionXTargetValue();
// get absolute position of FocusManager on screen
const px = this.core.renderContext.px;
const itemX = px + transitionX + x;
// _scissor consists of [ left position (x), top position (y), width, height ]
const [leftBounds = null, , clipWidth = null] = this.core._scissor || [];
const stageW = this.stage.w / this.stage.getRenderPrecision();
const { w } = child;
const withinLeftStageBounds = itemX >= 0;
const withinRightStageBounds = itemX + w <= stageW;
// short circuit
if (!withinLeftStageBounds || !withinRightStageBounds) return false;
let withinLeftClippingBounds = true;
let withinRightClippingBounds = true;
if (Number.isFinite(leftBounds)) {
withinLeftClippingBounds =
Math.round(itemX + w) >= Math.round(leftBounds);
withinRightClippingBounds =
Math.round(itemX) <= Math.round(leftBounds + clipWidth);
}
return withinLeftClippingBounds && withinRightClippingBounds;
}
_isComponentVerticallyVisible(child) {
// get child's destination Y; If child is moving to a destination,
// get the value of where child will end up
const y = getY(child);
if (!Number.isFinite(y)) return false;
// to calculate the target absolute Y position of the item, we need to use
// 1) the entire component's absolute position,
// 2) the target animation value of the items container, and
// 3) the target value of the item itself
const transitionY = this.getTransitionYTargetValue();
// get absolute position of FocusManager on screen
const py = this.core.renderContext.py;
// _scissor consists of [ left position (x), top position (y), width, height ]
const [, topBounds = null, , clipHeight = null] = this.core._scissor || [];
const { h } = child;
const itemY = py + transitionY + y;
const stageH = this.stage.h / this.stage.getRenderPrecision();
const withinTopStageBounds = itemY + h >= 0;
const withingBottomStageBounds = itemY <= stageH;
// short circuit
if (!withinTopStageBounds || !withingBottomStageBounds) return false;
let withinTopClippingBounds = true;
let withinBottomClippingBounds = true;
if (Number.isFinite(topBounds)) {
withinTopClippingBounds = Math.round(itemY + h) > Math.round(topBounds);
withinBottomClippingBounds =
Math.round(itemY) < Math.round(topBounds + clipHeight);
}
return withinTopClippingBounds && withinBottomClippingBounds;
}
getTransitionXTargetValue() {
return this.Items.transition('x').targetValue;
}
getTransitionYTargetValue() {
return this.Items.transition('y').targetValue;
}
static _states() {

@@ -251,0 +444,0 @@ return [

/**
* Copyright 2021 Comcast Cable Communications Management, LLC
* Copyright 2022 Comcast Cable Communications Management, LLC
*

@@ -20,4 +20,3 @@ * Licensed under the Apache License, Version 2.0 (the "License");

import FocusManager from '../FocusManager';
import { getX, getH } from '../../utils';
import { debounce } from 'debounce';
import { getX, getH, delayForAnimation } from '../../utils';
export default class Row extends FocusManager {

@@ -31,2 +30,17 @@ static _template() {

static get properties() {
return [
...super.properties,
'itemSpacing',
'scrollIndex',
'alwaysScroll',
'neverScroll',
'lazyScroll',
'lazyUpCount',
'autoResize',
'startLazyScrollIndex',
'stopLazyScrollIndex'
];
}
_construct() {

@@ -36,10 +50,3 @@ super._construct();

this._itemSpacing = 0;
this._itemPosX = 0;
this._itemPosY = 0;
this._scrollIndex = 0;
this._whenEnabled = new Promise(resolve => (this._firstEnable = resolve));
this.debounceDelay = Number.isInteger(this.debounceDelay)
? this.debounceDelay
: 1;
this._update = debounce(this._updateLayout, this.debounceDelay);
}

@@ -50,12 +57,36 @@

if (!this.w) {
// if width is undefinend or 0, set the Row's width
this.w =
this.parent && // if the Row is a child item in a FocusManager (like Column)
// if width is undefined or 0, set the Row's width
if (
// if the Row is a child item in a FocusManager (like Column)
this.parent &&
this.parent.parent &&
this.parent.parent instanceof FocusManager
? this.parent.parent.w
: this.stage.w;
this.parent.parent instanceof FocusManager &&
this.parent.parent.w
) {
this.w = this.parent.parent.w;
} else {
let parent = this.p;
while (parent && !parent.w) {
parent = parent.p;
}
this.w =
(parent && parent.w) ||
this.stage.h / this.stage.getRenderPrecision();
}
}
this.Items.transition('x').on(
'finish',
this._transitionListener.bind(this)
);
}
_update() {
this._updateLayout();
}
_setItemSpacing(itemSpacing) {
return itemSpacing || 0;
}
get _itemTransition() {

@@ -80,2 +111,7 @@ return (

this._smooth = true;
if (this._lazyItems && this._lazyItems.length) {
delayForAnimation(() => {
this.appendItems(this._lazyItems.splice(0, 1));
});
}
return super.selectNext();

@@ -119,45 +155,10 @@ }

get onScreenItems() {
return this.Items.children.filter(child => this._isOnScreen(child));
}
_isOnScreen(child) {
// Since this is a Row, scrolling should be done only when focused item (this.selected) is fully visible horizontally
// as scrolling is happening along X axis. So, if we have a row that has height greater than screen's height, it still
// can scroll. Method below the '_isComponentHorizontallyVisible' does not take "full" visibility into an account.
// At the same time, 'isFullyOnScreen' method on the Base class or utils method checks that element is fully visible
// both vertically and horizontally.
// At a later time, we should investigate this further.
_isOnScreenForScrolling(child) {
if (!child) return false;
const x = getX(child);
if (!Number.isFinite(x)) return false;
// to calculate the target absolute X position of the item, we need to use
// 1) the entire row's absolute position,
// 2) the target animation value of the items container, and
// 3) the target value of the item itself
const ItemX =
this.core.renderContext.px + this.Items.transition('x').targetValue + x;
const { w } = child;
// check that the child is inside the bounds of the stage
const withinLeftStageBounds = ItemX > 0;
// stage width needs to be adjusted with precision since all other values assume the original height and width (pre-scaling)
const withinRightStageBounds =
ItemX + w < this.stage.w / this.stage.getRenderPrecision();
// check that the child is inside the bounds of any clipping
let withinLeftClippingBounds = true;
let withinRightClippingBounds = true;
if (this.core._scissor && this.core._scissor.length) {
// _scissor consists of [ left position (x), top position (y), width, height ]
const leftBounds = this.core._scissor[0];
const rightBounds = leftBounds + this.core._scissor[2];
withinLeftClippingBounds = Math.round(ItemX + w) > Math.round(leftBounds);
withinRightClippingBounds = Math.round(ItemX) < Math.round(rightBounds);
}
return (
withinLeftStageBounds &&
withinRightStageBounds &&
withinLeftClippingBounds &&
withinRightClippingBounds
);
}
_isOnScreenCompletely(child) {
if (!child) return false;
const itemX = child.core.renderContext.px;

@@ -169,6 +170,16 @@ const rowX = this.core.renderContext.px;

_shouldScroll() {
let shouldScroll = this.alwaysScroll;
if (
this.lazyScroll &&
(this.selectedIndex <= this.startLazyScrollIndex ||
this.selectedIndex >= this.stopLazyScrollIndex)
) {
return true;
}
let shouldScroll = this.alwaysScroll || this._selectedPastAdded;
if (!shouldScroll && !this.neverScroll) {
const isCompletelyOnScreen = this._isOnScreenForScrolling(this.selected);
if (this.lazyScroll) {
shouldScroll = !this._isOnScreenCompletely(this.selected);
shouldScroll = !isCompletelyOnScreen;
} else {

@@ -180,3 +191,3 @@ const lastChild = this.Items.childList.last;

this.shouldScrollRight() ||
!this._isOnScreenCompletely(this.selected));
!isCompletelyOnScreen);
}

@@ -187,16 +198,56 @@ }

_getPrependedOffset() {
this._selectedPastAdded = false;
return this.Items.x - this._totalAddedWidth;
}
_getLazyScrollX(prev) {
let itemsContainerX;
const prevIndex = this.Items.childList.getIndex(prev);
if (prevIndex === this._lastFocusableIndex()) {
itemsContainerX = -this.Items.children[0].x;
} else if (prevIndex > this.selectedIndex) {
itemsContainerX = -this.selected.x;
} else if (prevIndex < this.selectedIndex) {
itemsContainerX = this.w - this.selected.x - this.selected.w;
const prevIndex = this.Items.childList.getIndex(this.prevSelected);
if (this._selectedPastAdded) {
return this._getPrependedOffset();
}
return itemsContainerX;
if (this.selectedIndex <= this.startLazyScrollIndex) {
// if navigating on items before start lazy scroll index, use normal scroll logic
return this._getScrollX();
} else if (
this.selectedIndex >= this.stopLazyScrollIndex &&
this.selectedIndex < prevIndex
) {
// if navigating left on items after stop lazy scroll index, only shift by size of prev item
const currItemsX = this.Items.transition('x')
? this.Items.transition('x').targetValue
: this.Items.x;
return (
currItemsX +
(this.prevSelected.w +
this.itemSpacing +
(this.selected.extraItemSpacing || 0))
);
} else if (prev) {
// otherwise, no start/stop indexes, perform normal lazy scroll
let itemsContainerX;
const prevIndex = this.Items.childList.getIndex(prev);
if (prevIndex === -1) {
// No matches found in childList, start set x to 0
return;
}
if (prevIndex > this.selectedIndex) {
itemsContainerX = -this.selected.x;
} else if (prevIndex < this.selectedIndex) {
itemsContainerX = this.w - this.selected.x - this.selected.w;
}
return itemsContainerX;
}
// if no prev item or start/stop index, default to normal scroll logic
return this._getScrollX();
}
_getScrollX() {
if (this._selectedPastAdded) {
return this._getPrependedOffset();
}
let itemsContainerX;

@@ -208,2 +259,3 @@ let itemIndex = this.selectedIndex - this.scrollIndex;

}
if (this.Items.children[itemIndex]) {

@@ -214,2 +266,3 @@ itemsContainerX = this.Items.children[itemIndex].transition('x')

}
return itemsContainerX;

@@ -230,6 +283,5 @@ }

} else if (this._shouldScroll()) {
itemsContainerX =
this.lazyScroll && prev
? this._getLazyScrollX(prev)
: this._getScrollX();
itemsContainerX = this.lazyScroll
? this._getLazyScrollX(prev)
: this._getScrollX();
}

@@ -243,2 +295,3 @@ if (itemsContainerX !== undefined) {

this.Items.x = itemsContainerX;
this._updateTransitionTarget(this.Items, 'x', itemsContainerX);
}

@@ -262,6 +315,8 @@ }

child.patch({ x: nextX });
this._updateTransitionTarget(child, 'x', nextX);
}
nextX += child.w;
if (i < this.Items.children.length - 1) {
nextX += this.itemSpacing;
const extraItemSpacing = child.extraItemSpacing || 0;
nextX += this.itemSpacing + extraItemSpacing;
}

@@ -278,4 +333,11 @@

}
this.Items.patch({ h: nextH, w: nextX });
const itemChanged = this.Items.h !== nextH || this.Items.w !== nextX;
this.Items.patch({ h: nextH, w: nextX + (this._totalAddedWidth || 0) });
if (this.autoResize) {
this.h = this.Items.h;
this.w = this.Items.w;
}
const lastChild = this.Items.childList.last;

@@ -299,60 +361,78 @@ const endOfLastChild = lastChild ? getX(lastChild) + lastChild.w : 0;

}
this.fireAncestors('$itemChanged');
itemChanged && this.fireAncestors('$itemChanged');
this.render(this.selected, this.prevSelected);
}
get itemSpacing() {
return this._itemSpacing;
get _itemsX() {
return getX(this.Items);
}
set itemSpacing(itemSpacing) {
if (itemSpacing !== this._itemSpacing) {
this._itemSpacing = itemSpacing;
this._update();
}
}
_isOnScreen(child) {
if (!child) return false;
get scrollIndex() {
return this._scrollIndex;
return this._isComponentHorizontallyVisible(child);
}
set scrollIndex(scrollIndex) {
if (scrollIndex !== this._scrollIndex) {
this._scrollIndex = scrollIndex;
this._update();
appendItems(items = []) {
const itemHeight = this.renderHeight;
this._smooth = false;
if (items.length > this.lazyUpCount + 2) {
this._lazyItems = items.splice(this.lazyUpCount + 2);
}
}
set itemPosX(x) {
this.Items.x = this._itemPosX = x;
items.forEach(item => {
item.parentFocus = this.hasFocus();
item = this.Items.childList.a(item);
item.h = item.h || itemHeight;
});
this.stage.update();
this._update();
this._refocus();
}
get itemPosX() {
return this._itemPosX;
}
appendItemsAt(items = [], idx) {
const addIndex = Number.isInteger(idx) ? idx : this.Items.children.length;
this._smooth = false;
this._lastAppendedIdx = addIndex;
this._totalAddedWidth = 0;
set itemPosY(y) {
this.Items.y = this._itemPosY = y;
}
items.forEach((item, itemIdx) => {
this.Items.childList.addAt(
{
...item,
parentFocus: this.hasFocus(),
h: item.h || this.Items.h
},
addIndex + itemIdx
);
const extraItemSpacing = item.extraItemSpacing || 0;
this._totalAddedWidth += item.w + this.itemSpacing + extraItemSpacing;
});
get itemPosY() {
return this._itemPosY;
if (this.selectedIndex >= this._lastAppendedIdx) {
this._selectedPastAdded = true;
this._selectedIndex += items.length;
}
this._update();
this._refocus();
}
get _itemsX() {
return getX(this.Items);
prependItems(items) {
this.appendItemsAt(items, 0);
}
appendItems(items = []) {
const itemHeight = this.renderHeight;
removeItemAt(index) {
this._smooth = false;
this.Items.childList.removeAt(index);
items.forEach(item => {
item.parentFocus = this.hasFocus();
item = this.Items.childList.a(item);
item.h = item.h || itemHeight;
});
this.stage.update();
this._updateLayout();
this._update.clear();
if (
this.selectedIndex > index ||
this.selectedIndex === this.Items.children.length
) {
this._selectedIndex--;
}
this._update();
this._refocus();

@@ -365,4 +445,12 @@ }

_transitionListener() {
this._smooth = false;
this.transitionDone();
}
// can be overridden
onScreenEffect() {}
// can be overridden
transitionDone() {}
}
/**
* Copyright 2021 Comcast Cable Communications Management, LLC
* Copyright 2022 Comcast Cable Communications Management, LLC
*

@@ -366,1 +366,253 @@ * Licensed under the Apache License, Version 2.0 (the "License");

};
export const LazyScrollIndexes = ({
startLazyScrollIndex,
stopLazyScrollIndex
}) =>
class LazyScrollIndexes extends lng.Component {
static _template() {
return {
Row: {
type: Row,
w: 1920 - 160, // x offset from preview.js * 2
itemSpacing: 20,
alwaysScroll: false,
neverScroll: false,
lazyScroll: true,
scrollIndex: 0,
items: Array.apply(null, { length: 12 }).map((_, i) => ({
type: Button,
buttonText: `Button ${i + 1} ${
i === startLazyScrollIndex ? '(start lazy scroll)' : ''
} ${i === stopLazyScrollIndex ? '(stop lazy scroll)' : ''}`,
w: 250
})),
startLazyScrollIndex,
stopLazyScrollIndex
}
};
}
_getFocused() {
return this.tag('Row');
}
};
LazyScrollIndexes.args = {
startLazyScrollIndex: 1,
stopLazyScrollIndex: 10
};
LazyScrollIndexes.argTypes = {
startLazyScrollIndex: {
control: 'number'
},
stopLazyScrollIndex: {
control: 'number'
}
};
export const AddingItems = args =>
class AddingItems extends lng.Component {
static _template() {
return {
Row: {
type: Row,
w: 1920 - 160, // x offset from preview.js * 2
itemSpacing: args.itemSpacing,
alwaysScroll: args.alwaysScroll,
neverScroll: args.neverScroll,
lazyScroll: args.lazyScroll,
scrollIndex: args.scrollIndex,
items: Array.apply(null, { length: 12 }).map((_, i) => ({
type: Button,
buttonText: `Button ${i}`,
w: 150
}))
}
};
}
_init() {
super._init();
setTimeout(() => {
this.tag('Row').appendItemsAt(
[
{
type: Button,
buttonText: 'New Button 0',
w: 150
},
{
type: Button,
buttonText: 'New Button 1',
w: 150
},
{
type: Button,
buttonText: 'New Button 2',
w: 150
}
],
3
);
}, 3000);
setTimeout(() => {
this.tag('Row').prependItems([
{
type: Button,
buttonText: 'New Button 3',
w: 150
},
{
type: Button,
buttonText: 'New Button 4',
w: 150
},
{
type: Button,
buttonText: 'New Button 5',
w: 150
}
]);
}, 3750);
}
_getFocused() {
return this.tag('Row');
}
};
AddingItems.args = {
itemSpacing: 20,
scrollIndex: 0,
alwaysScroll: false,
neverScroll: false,
lazyScroll: false
};
AddingItems.argTypes = {
itemSpacing: {
control: { type: 'range', min: 0, max: 100, step: 5 }
},
scrollIndex: {
control: 'number'
},
alwaysScroll: {
control: 'boolean'
},
neverScroll: {
control: 'boolean'
},
lazyScroll: {
control: 'boolean'
}
};
export const LazyUpCount = args =>
class LazyUpCount extends lng.Component {
static _template() {
return {
Row: {
type: Row,
w: 1920 - 160, // x offset from preview.js * 2
itemSpacing: args.itemSpacing,
alwaysScroll: args.alwaysScroll,
neverScroll: args.neverScroll,
lazyScroll: args.lazyScroll,
lazyUpCount: args.lazyUpCount,
scrollIndex: args.scrollIndex,
items: Array.apply(null, { length: 12 }).map((_, i) => ({
type: Button,
buttonText: `Button ${i + 1}`,
w: 150
}))
}
};
}
_getFocused() {
return this.tag('Row');
}
};
LazyUpCount.args = {
itemSpacing: 20,
scrollIndex: 0,
alwaysScroll: false,
neverScroll: false,
lazyScroll: false,
lazyUpCount: 4
};
LazyUpCount.argTypes = {
itemSpacing: {
control: { type: 'range', min: 0, max: 100, step: 5 }
},
scrollIndex: {
control: 'number'
},
lazyUpCount: {
control: 'number'
},
alwaysScroll: {
control: 'boolean'
},
neverScroll: {
control: 'boolean'
},
lazyScroll: {
control: 'boolean'
}
};
export const RemovingItems = args =>
class RemovingItems extends lng.Component {
static _template() {
return {
Row: {
type: Row,
w: 1920 - 160, // x offset from preview.js * 2
itemSpacing: args.itemSpacing,
alwaysScroll: args.alwaysScroll,
neverScroll: args.neverScroll,
lazyScroll: args.lazyScroll,
scrollIndex: args.scrollIndex,
items: ['A', 'B', 'C', 'D', 'E'].map(letter => ({
type: Button,
buttonText: letter,
w: 150
}))
}
};
}
_init() {
super._init();
setTimeout(() => {
this.tag('Row').removeItemAt(1);
}, 3000);
}
_getFocused() {
return this.tag('Row');
}
};
RemovingItems.args = {
itemSpacing: 20,
scrollIndex: 0,
alwaysScroll: false,
neverScroll: false,
lazyScroll: false
};
RemovingItems.argTypes = {
itemSpacing: {
control: { type: 'range', min: 0, max: 100, step: 5 }
},
scrollIndex: {
control: 'number'
},
alwaysScroll: {
control: 'boolean'
},
neverScroll: {
control: 'boolean'
},
lazyScroll: {
control: 'boolean'
}
};
{
"name": "@lightningjs/ui-components",
"version": "1.3.0-beta.1",
"version": "1.3.0",
"dependencies": {

@@ -9,3 +9,3 @@ "debounce": "^1.2.1"

"peerDependencies": {
"@lightningjs/core": "^2.x"
"@lightningjs/core": "^2.1.1"
},

@@ -18,28 +18,28 @@ "browser": "index.js",

"Styles.js",
"{bin,elements,layout,mixins,Styles,utils}/**/*",
"!elements|layout|Styles/**/*.stories.js",
"!{elements,layout,utils}/**/*.test.js",
"!elements|layout/lightning-test-renderer.js",
"!elements|layout/lightning-test-utils.js",
"!elements|layout/**/__snapshots__",
"{bin,elements,layout,mixins,Styles,textures,utils}/**/*",
"!{elements,layout,mixins,Styles,textures,utils}/**/*.stories.js",
"!{elements,layout,mixins,Styles,textures,utils}/**/*.test.js",
"test/lightning-test-renderer.js",
"test/lightning-test-utils.js",
"!{elements,layout,mixins,Styles,textures,utils}/**/__snapshots__",
"!public/"
],
"devDependencies": {
"@babel/core": "^7.10.5",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-transform-modules-commonjs": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@commitlint/cli": "^9.1.2",
"@commitlint/config-conventional": "^8.3.4",
"@lightningjs/core": "^2.3.0",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0",
"@storybook/addon-docs": "^6.3.10",
"@storybook/addon-essentials": "^6.3.10",
"@storybook/addon-storysource": "^6.3.10",
"@storybook/html": "^6.3.10",
"@babel/core": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-transform-modules-commonjs": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.13.10",
"@babel/preset-env": "^7.13.10",
"@babel/preset-react": "^7.12.10",
"@commitlint/cli": "^17.0.0",
"@commitlint/config-conventional": "^17.0.0",
"@lightningjs/core": "2.5.0",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1",
"@storybook/addon-docs": "^6.5.3",
"@storybook/addon-essentials": "^6.5.3",
"@storybook/addon-storysource": "^6.5.3",
"@storybook/html": "^6.5.3",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-loader": "^8.2.2",
"canvas": "^2.7.0",

@@ -50,10 +50,9 @@ "eslint": "^7.22.0",

"eslint-plugin-prettier": "^3.3.1",
"gh-pages": "^3.1.0",
"husky": "^3.1.0",
"jest": "^24.9.0",
"jest-environment-jsdom-fifteen": "^1.0.2",
"gh-pages": "^4.0.0",
"husky": "^8.0.1",
"jest": "^26.6.1",
"jest-webgl-canvas-mock": "^0.2.3",
"lint-staged": "^9.5.0",
"prettier": "^1.19.1",
"semantic-release": "^17.1.1"
"lint-staged": "^12.1.2",
"prettier": "^2.4.1",
"semantic-release": "^19.0.2"
},

@@ -60,0 +59,0 @@ "husky": {

/**
* Copyright 2020 Comcast Cable Communications Management, LLC
* Copyright 2022 Comcast Cable Communications Management, LLC
*

@@ -115,3 +115,3 @@ * Licensed under the Apache License, Version 2.0 (the "License");

const value = object[key];
if (Object.prototype.hasOwnProperty.call(target, key)) {
if (target.hasOwnProperty(key)) {
_clone[key] = getMergeValue(key, target, object);

@@ -191,8 +191,8 @@ } else {

/**
* Returns an array of strings and icon or badge objects from a string using the syntax:
* 'This is a {ICON:<title>|<url>} and {BADGE:<title>} badge test.'
* Returns an array of strings and icon, badge, newline, and text objects from a string using the syntax:
* 'This is an {ICON:<title>|<url>} and {BADGE:<title>} badge test with a {NEWLINE} newline and {TEXT:<text>|<style>}.'
*
* i.e. 'This is an {ICON:settings|./assets/icons/settings.png} icon and {BADGE:<HD>} badge.'
* would create the object:
* {
* i.e. 'This is an {ICON:settings|./assets/icons/settings.png} icon and {BADGE:HD} badge with a{NEWLINE} and {TEXT:red text|red}.'
* would create the array:
* [
* 'This is an ',

@@ -202,6 +202,10 @@ * { icon: './assets/icons/settings.png', title: 'settings' },

* { badge: 'HD' },
* ' badge.'
* }
* ' badge with a',
* { newline: true },
* ' and ',
* { text: 'red text', style: 'red' },
* '.'
* ]
*
* @param {*} str
* @param {(string|object)} str
*

@@ -212,8 +216,12 @@ * @return {array}

const content = [];
if (str && typeof str === 'string') {
const regex = /({ICON.*?}|{BADGE:.*?})/g;
if ((str && typeof str === 'string') || str.text) {
const string = typeof str === 'string' ? str : str.text;
const regex = /({ICON.*?}|{BADGE:.*?}|{NEWLINE}|{TEXT:.*?})/g;
const iconRegEx = /^{ICON:(.*?)?\|(.*?)?}$/g;
const badgeRegEx = /^{BADGE:(.*?)}$/g;
const iconRegEx = /^{ICON:(.*?)?\|(.*?)?}$/g;
const splitStr = str.split(regex);
const newlineRegEx = /^{NEWLINE}$/g;
const textRegEx = /^{TEXT:(.*?)?\|(.*?)?}$/g;
const splitStr = string.split(regex);
if (splitStr && splitStr.length) {

@@ -224,8 +232,13 @@ splitStr.forEach(item => {

const icon = iconRegEx.exec(item);
const newline = newlineRegEx.exec(item);
const text = textRegEx.exec(item);
if (badge && badge[1]) {
formattedItem = { badge: badge[1] };
}
if (icon && icon[1]) {
} else if (icon && icon[1]) {
formattedItem = { title: icon[1], icon: icon[2] || icon[1] };
} else if (newline) {
formattedItem = { newline: true };
} else if (text && text[1]) {
formattedItem = { text: text[1], style: text[2] };
}

@@ -274,12 +287,58 @@ content.push(formattedItem);

/**
* Deep equality check two values
*
* @param {any} valA - value to be compared against valB
* @param {any} valB - value to be compared against valA
*
* @return {boolean} - returns true if values are equal
*/
export function stringifyCompare(valA, valB) {
return JSON.stringify(valA) === JSON.stringify(valB);
export function objectPropertyOf(object, path) {
return path.reduce(
(obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : undefined),
object
);
}
export function stringifyCompare(objA, objB) {
return JSON.stringify(objA) === JSON.stringify(objB);
}
export function isComponentOnScreen(component) {
if (!component) return false;
const {
w,
h,
core: { renderContext: { px, py }, _scissor: scissor = [] } = {}
} = component;
const stageH = component.stage.h / component.stage.getRenderPrecision();
const stageW = component.stage.w / component.stage.getRenderPrecision();
const wVis = px >= 0 && px + w <= stageW;
const hVis = py >= 0 && py + h <= stageH;
if (!wVis || !hVis) return false;
if (scissor && scissor.length) {
const [
leftBounds = null,
topBounds = null,
clipWidth = null,
clipHeight = null
] = scissor;
const withinLeftClippingBounds =
Math.round(px + w) >= Math.round(leftBounds);
const withinRightClippingBounds =
Math.round(px) <= Math.round(leftBounds + clipWidth);
const withinTopClippingBounds = Math.round(py + h) >= Math.round(topBounds);
const withinBottomClippingBounds =
Math.round(py + h) <= Math.round(topBounds + clipHeight);
return (
withinLeftClippingBounds &&
withinRightClippingBounds &&
withinTopClippingBounds &&
withinBottomClippingBounds
);
}
return true;
}
export function delayForAnimation(callback, delay = 16) {
setTimeout(callback, delay);
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc