@enact/sandstone
Version:
Large-screen/TV support library for Enact, containing a variety of UI components.
475 lines (456 loc) • 23.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useThemeVirtualList = exports["default"] = void 0;
var _spotlight = _interopRequireWildcard(require("@enact/spotlight"));
var _Accelerator = _interopRequireDefault(require("@enact/spotlight/Accelerator"));
var _Pause = _interopRequireDefault(require("@enact/spotlight/Pause"));
var _target = require("@enact/spotlight/src/target");
var _Spottable = require("@enact/spotlight/Spottable");
var _resolution = _interopRequireDefault(require("@enact/ui/resolution"));
var _utilDOM = _interopRequireDefault(require("@enact/ui/useScroll/utilDOM"));
var _react = require("react");
var _useScroll = require("../useScroll");
var _useEvent = require("./useEvent");
var _usePreventScroll = _interopRequireDefault(require("./usePreventScroll"));
var _useSpotlight = require("./useSpotlight");
var _useThemeVirtualListModule = _interopRequireDefault(require("./useThemeVirtualList.module.css"));
var _jsxRuntime = require("react/jsx-runtime");
var _excluded = ["itemRenderer", "data-webos-voice-focused", "data-webos-voice-group-label", "data-webos-voice-disabled"],
_excluded2 = ["index"];
var _this = void 0;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
var SpotlightAccelerator = new _Accelerator["default"]();
var SpotlightPlaceholder = (0, _Spottable.Spottable)('div');
var nop = function nop() {},
getNumberValue = function getNumberValue(index) {
// using '+ operator' for string > number conversion based on performance: https://jsperf.com/convert-string-to-number-techniques/7
var number = +index;
// should return -1 if index is not a number or a negative value
return number >= 0 ? number : -1;
};
var useSpottable = function useSpottable(props, instances) {
var noAffordance = props.noAffordance,
scrollMode = props.scrollMode,
snapToCenter = props.snapToCenter;
var itemRefs = instances.itemRefs,
scrollContainerRef = instances.scrollContainerRef,
scrollContentHandle = instances.scrollContentHandle;
var getItemNode = function getItemNode(index) {
var itemNode = itemRefs.current[index % scrollContentHandle.current.state.numOfItems];
return itemNode && parseInt(itemNode.dataset.index) === index ? itemNode : null;
};
// Mutable value
var mutableRef = (0, _react.useRef)({
dataSize: 0,
isScrolledBy5way: false,
isScrolledByJump: false,
isScrollingBySnapToCenter: false,
isWrappedBy5way: false,
lastFocusedIndex: null,
pause: new _Pause["default"]('VirtualListBasic'),
scaledTarget: null
});
var pause = mutableRef.current.pause;
// Hooks
(0, _useSpotlight.useSpotlightConfig)(props, {
spottable: mutableRef
});
var _useEventKey = (0, _useEvent.useEventKey)(props, instances, {
handlePageUpDownKeyDown: function handlePageUpDownKeyDown() {
mutableRef.current.isScrolledBy5way = false;
},
handleDirectionKeyDown: function handleDirectionKeyDown(ev, eventType, param) {
switch (eventType) {
case 'acceleratedKeyDown':
onAcceleratedKeyDown(param);
break;
case 'keyDown':
if (_spotlight["default"].move(param.direction)) {
var nextTargetIndex = _spotlight["default"].getCurrent().dataset.index;
ev.preventDefault();
ev.stopPropagation();
if (typeof nextTargetIndex === 'string') {
onAcceleratedKeyDown(_objectSpread(_objectSpread({}, param), {}, {
nextIndex: getNumberValue(nextTargetIndex)
}));
}
}
break;
case 'keyLeave':
SpotlightAccelerator.reset();
break;
}
},
handle5WayKeyUp: function handle5WayKeyUp() {
SpotlightAccelerator.reset();
},
spotlightAcceleratorProcessKey: function spotlightAcceleratorProcessKey(ev) {
return SpotlightAccelerator.processKey(ev, nop);
}
}),
addGlobalKeyDownEventListener = _useEventKey.addGlobalKeyDownEventListener,
removeGlobalKeyDownEventListener = _useEventKey.removeGlobalKeyDownEventListener;
(0, _useEvent.useEventFocus)(props, instances, {
removeScaleEffect: removeScaleEffect.bind(_this)
});
var _useSpotlightRestore = (0, _useSpotlight.useSpotlightRestore)(props, _objectSpread(_objectSpread({}, instances), {}, {
spottable: mutableRef
}), {
focusByIndex: focusByIndex,
getItemNode: getItemNode
}),
handlePlaceholderFocus = _useSpotlightRestore.handlePlaceholderFocus,
handleRestoreLastFocus = _useSpotlightRestore.handleRestoreLastFocus,
setPreservedIndex = _useSpotlightRestore.setPreservedIndex,
updateStatesAndBounds = _useSpotlightRestore.updateStatesAndBounds;
function pauseSpotlight(bool) {
if (bool) {
pause.pause();
} else {
pause.resume();
}
}
var setContainerDisabled = (0, _react.useCallback)(function (bool) {
if (scrollContainerRef.current) {
scrollContainerRef.current.dataset.spotlightContainerDisabled = bool;
if (bool) {
addGlobalKeyDownEventListener(handleGlobalKeyDown);
} else {
removeGlobalKeyDownEventListener();
}
}
}, [addGlobalKeyDownEventListener, handleGlobalKeyDown, removeGlobalKeyDownEventListener, scrollContainerRef]);
// eslint-disable-next-line react-hooks/exhaustive-deps
function handleGlobalKeyDown(ev) {
// To prevent scrolling by native scroller
if (scrollMode === 'native') {
ev.preventDefault();
ev.stopPropagation();
}
setContainerDisabled(false);
}
(0, _react.useEffect)(function () {
if (scrollContainerRef.current && scrollContainerRef.current.dataset.spotlightContainerDisabled === 'true') {
removeGlobalKeyDownEventListener();
addGlobalKeyDownEventListener(handleGlobalKeyDown);
}
}, [handleGlobalKeyDown]); // eslint-disable-line react-hooks/exhaustive-deps
(0, _react.useEffect)(function () {
return function () {
pause.resume();
SpotlightAccelerator.reset();
setContainerDisabled(false);
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (props.dataSize !== mutableRef.current.dataSize) {
var _current$dataset;
var current = _spotlight["default"].getCurrent();
if (current && props.scrollContainerContainsDangerously(current) && ((_current$dataset = current.dataset) === null || _current$dataset === void 0 ? void 0 : _current$dataset.index) > props.dataSize - 1) {
// if a focused item is about to disappear
setPreservedIndex(props.dataSize - 1);
}
mutableRef.current.dataSize = props.dataSize;
}
// Functions
function onAcceleratedKeyDown(_ref) {
var isWrapped = _ref.isWrapped,
keyCode = _ref.keyCode,
nextIndex = _ref.nextIndex,
repeat = _ref.repeat,
target = _ref.target;
var cbScrollTo = props.cbScrollTo,
dataSize = props.dataSize,
wrap = props.wrap,
orientation = props.direction;
var _scrollContentHandle$ = scrollContentHandle.current,
dimensionToExtent = _scrollContentHandle$.dimensionToExtent,
_scrollContentHandle$2 = _scrollContentHandle$.primary,
clientSize = _scrollContentHandle$2.clientSize,
itemSize = _scrollContentHandle$2.itemSize,
scrollPosition = _scrollContentHandle$.scrollPosition,
scrollPositionTarget = _scrollContentHandle$.scrollPositionTarget;
var index = getNumberValue(target.dataset.index);
var direction = (0, _spotlight.getDirection)(keyCode);
var allowAffordance = !(noAffordance || orientation === 'horizontal');
var shouldMove = snapToCenter ? nextIndex > 0 && nextIndex < dataSize - 1 && index > 0 : nextIndex >= 0 && index >= 0;
mutableRef.current.isScrolledBy5way = false;
mutableRef.current.isScrolledByJump = false;
if (shouldMove) {
var row = Math.floor(index / dimensionToExtent),
nextRow = Math.floor(nextIndex / dimensionToExtent),
start = scrollContentHandle.current.getGridPosition(nextIndex).primaryPosition,
end = props.itemSizes ? scrollContentHandle.current.getItemBottomPosition(nextIndex) : start + itemSize,
startBoundary = scrollMode === 'native' ? scrollPosition : scrollPositionTarget,
endBoundary = startBoundary + clientSize - (!allowAffordance ? 0 : _resolution["default"].scale(_useScroll.affordanceSize));
mutableRef.current.lastFocusedIndex = nextIndex;
if (start >= startBoundary && end <= endBoundary) {
// The next item could be still out of viewport. So we need to prevent scrolling into view with `isScrolledBy5way` flag.
mutableRef.current.isScrolledBy5way = true;
focusByIndex(nextIndex, direction);
mutableRef.current.isScrolledBy5way = false;
} else if (row === nextRow) {
focusByIndex(nextIndex, direction);
} else if (!snapToCenter || !mutableRef.current.isScrollingBySnapToCenter) {
var itemNode = getItemNode(nextIndex);
var stickTo = Math.abs(endBoundary - end) < Math.abs(startBoundary - start) ? 'end' : 'start';
stickTo = snapToCenter ? 'center' : stickTo;
mutableRef.current.isScrolledBy5way = true;
mutableRef.current.isWrappedBy5way = isWrapped;
if (snapToCenter) {
mutableRef.current.isScrollingBySnapToCenter = true;
}
if (isWrapped && wrap === true && itemNode === null) {
pause.pause();
target.blur();
}
focusByIndex(nextIndex, direction, true);
cbScrollTo({
index: nextIndex,
stickTo: stickTo,
offset: allowAffordance && stickTo === 'end' ? _resolution["default"].scale(_useScroll.affordanceSize) : 0,
disallowNegativeOffset: true,
animate: !(isWrapped && wrap === 'noAnimation'),
focus: snapToCenter
});
}
} else if (!repeat && _spotlight["default"].move(direction)) {
SpotlightAccelerator.reset();
}
}
function focusOnNode(node) {
if (node) {
return _spotlight["default"].focus(node);
}
return false;
}
function focusByIndex(index, direction, waiting) {
var itemNode = getItemNode(index);
var returnVal = false;
if (!itemNode && index >= 0 && index < props.dataSize) {
// Item is valid but since the the dom doesn't exist yet, we set the index to focus after the ongoing update
setPreservedIndex(index, direction);
} else {
var _current = _spotlight["default"].getCurrent(),
candidate = _current ? (0, _target.getTargetByDirectionFromElement)(direction, _current) : itemNode;
// Remove any preservedIndex
setPreservedIndex(-1);
if (mutableRef.current.isWrappedBy5way) {
SpotlightAccelerator.reset();
mutableRef.current.isWrappedBy5way = false;
}
pause.resume();
if (_utilDOM["default"].containsDangerously(itemNode, candidate)) {
returnVal = focusOnNode(candidate);
} else if (!mutableRef.current.isScrollingBySnapToCenter || props.scrollContainerContainsDangerously(_current)) {
returnVal = focusOnNode(itemNode);
}
mutableRef.current.isScrolledBy5way = false;
mutableRef.current.isScrolledByJump = false;
if (!waiting) {
mutableRef.current.isScrollingBySnapToCenter = false;
}
}
return returnVal;
}
function calculatePositionOnFocus(_ref2) {
var item = _ref2.item,
_ref2$scrollPosition = _ref2.scrollPosition,
scrollPosition = _ref2$scrollPosition === void 0 ? scrollContentHandle.current.scrollPosition : _ref2$scrollPosition;
var pageScroll = props.pageScroll,
direction = props.direction;
var _scrollContentHandle$3 = scrollContentHandle.current,
numOfItems = _scrollContentHandle$3.state.numOfItems,
primary = _scrollContentHandle$3.primary;
var allowAffordance = !(noAffordance || direction === 'horizontal');
var offsetToClientEnd = Math.max(0, primary.clientSize - (snapToCenter ? primary.gridSize : primary.itemSize) - (!allowAffordance ? 0 : _resolution["default"].scale(_useScroll.affordanceSize)));
var focusedIndex = getNumberValue(item.getAttribute(_useScroll.dataIndexAttribute));
var offsetToCenter = snapToCenter ? primary.clientSize / 2 - primary.gridSize / 2 : 0;
if (focusedIndex >= 0) {
var gridPosition = scrollContentHandle.current.getGridPosition(focusedIndex);
if (numOfItems > 0 && focusedIndex % numOfItems !== mutableRef.current.lastFocusedIndex % numOfItems) {
var itemNode = getItemNode(mutableRef.current.lastFocusedIndex);
if (itemNode) {
itemNode.blur();
}
}
mutableRef.current.lastFocusedIndex = focusedIndex;
if (primary.clientSize >= primary.itemSize) {
if (gridPosition.primaryPosition > scrollPosition + offsetToClientEnd) {
// forward over
gridPosition.primaryPosition -= pageScroll ? 0 : offsetToClientEnd - offsetToCenter;
} else if (gridPosition.primaryPosition >= scrollPosition) {
// inside of client
if (scrollMode === 'translate') {
gridPosition.primaryPosition = scrollPosition;
} else {
// This code uses the trick to change the target position slightly which will not affect the actual result
// since a browser ignore `scrollTo` method if the target position is same as the current position.
gridPosition.primaryPosition = scrollPosition + (scrollContentHandle.current.scrollPosition === scrollPosition ? 0.1 : 0);
}
} else {
// backward over
gridPosition.primaryPosition -= pageScroll ? offsetToClientEnd : offsetToCenter;
}
}
// Since the result is used as a target position to be scrolled,
// scrondaryPosition should be 0 here.
gridPosition.secondaryPosition = 0;
return scrollContentHandle.current.gridPositionToItemPosition(gridPosition);
}
}
function shouldPreventScrollByFocus() {
return scrollMode === 'translate' ? mutableRef.current.isScrolledBy5way : mutableRef.current.isScrolledBy5way || mutableRef.current.isScrolledByJump;
}
function shouldPreventOverscrollEffect() {
return mutableRef.current.isWrappedBy5way;
}
function setLastFocusedNode(node) {
mutableRef.current.lastFocusedIndex = node.dataset && getNumberValue(node.dataset.index);
}
function resetSnapToCenterStatus() {
mutableRef.current.isScrollingBySnapToCenter = false;
}
function addScaleEffect(elem) {
elem.classList.add(_useThemeVirtualListModule["default"].scaled);
mutableRef.current.scaledTarget = elem;
}
function removeScaleEffect() {
if (mutableRef.current.scaledTarget) {
mutableRef.current.scaledTarget.classList.remove(_useThemeVirtualListModule["default"].scaled);
mutableRef.current.scaledTarget = null;
}
}
function getScrollBounds() {
return scrollContentHandle.current.getScrollBounds();
}
// Return
return {
addScaleEffect: addScaleEffect,
calculatePositionOnFocus: calculatePositionOnFocus,
focusByIndex: focusByIndex,
focusOnNode: focusOnNode,
getScrollBounds: getScrollBounds,
handlePlaceholderFocus: handlePlaceholderFocus,
handleRestoreLastFocus: handleRestoreLastFocus,
pauseSpotlight: pauseSpotlight,
removeScaleEffect: removeScaleEffect,
resetSnapToCenterStatus: resetSnapToCenterStatus,
setContainerDisabled: setContainerDisabled,
setLastFocusedNode: setLastFocusedNode,
shouldPreventOverscrollEffect: shouldPreventOverscrollEffect,
shouldPreventScrollByFocus: shouldPreventScrollByFocus,
updateStatesAndBounds: updateStatesAndBounds
};
};
var useThemeVirtualList = exports.useThemeVirtualList = function useThemeVirtualList(props) {
var itemRefs = props.itemRefs,
scrollContainerRef = props.scrollContainerRef,
scrollContentHandle = props.scrollContentHandle,
scrollContentRef = props.scrollContentRef;
// Hooks
var instance = {
itemRefs: itemRefs,
scrollContainerRef: scrollContainerRef,
scrollContentHandle: scrollContentHandle,
scrollContentRef: scrollContentRef
};
var _useSpottable = useSpottable(props, instance),
addScaleEffect = _useSpottable.addScaleEffect,
calculatePositionOnFocus = _useSpottable.calculatePositionOnFocus,
focusByIndex = _useSpottable.focusByIndex,
focusOnNode = _useSpottable.focusOnNode,
getScrollBounds = _useSpottable.getScrollBounds,
handlePlaceholderFocus = _useSpottable.handlePlaceholderFocus,
handleRestoreLastFocus = _useSpottable.handleRestoreLastFocus,
pauseSpotlight = _useSpottable.pauseSpotlight,
removeScaleEffect = _useSpottable.removeScaleEffect,
resetSnapToCenterStatus = _useSpottable.resetSnapToCenterStatus,
setContainerDisabled = _useSpottable.setContainerDisabled,
setLastFocusedNode = _useSpottable.setLastFocusedNode,
shouldPreventOverscrollEffect = _useSpottable.shouldPreventOverscrollEffect,
shouldPreventScrollByFocus = _useSpottable.shouldPreventScrollByFocus,
updateStatesAndBounds = _useSpottable.updateStatesAndBounds;
(0, _usePreventScroll["default"])(props, instance);
var handle = {
addScaleEffect: addScaleEffect,
calculatePositionOnFocus: calculatePositionOnFocus,
focusByIndex: focusByIndex,
focusOnNode: focusOnNode,
getScrollBounds: getScrollBounds,
pauseSpotlight: pauseSpotlight,
removeScaleEffect: removeScaleEffect,
resetSnapToCenterStatus: resetSnapToCenterStatus,
setContainerDisabled: setContainerDisabled,
setLastFocusedNode: setLastFocusedNode,
shouldPreventOverscrollEffect: shouldPreventOverscrollEffect,
shouldPreventScrollByFocus: shouldPreventScrollByFocus
};
props.setThemeScrollContentHandle(handle);
function getAffordance() {
// To add space for the last item margin bottom
return props.noAffordance ? 0 : _resolution["default"].scale(30);
}
// Render
var _itemRenderer = props.itemRenderer,
voiceFocused = props['data-webos-voice-focused'],
voiceGroupLabel = props['data-webos-voice-group-label'],
voiceDisabled = props['data-webos-voice-disabled'],
rest = _objectWithoutProperties(props, _excluded);
delete rest.noAffordance;
delete rest.scrollContainerContainsDangerously;
delete rest.scrollContainerHandle;
delete rest.scrollContainerRef;
delete rest.scrollContentHandle;
delete rest.snapToCenter;
delete rest.spotlightId;
delete rest.wrap;
return _objectSpread(_objectSpread({}, rest), {}, {
containerProps: {
'data-webos-voice-focused': voiceFocused,
'data-webos-voice-group-label': voiceGroupLabel,
'data-webos-voice-disabled': voiceDisabled
},
getAffordance: getAffordance,
itemRenderer: function itemRenderer(_ref3) {
var index = _ref3.index,
itemRest = _objectWithoutProperties(_ref3, _excluded2);
return _itemRenderer(_objectSpread(_objectSpread({}, itemRest), {}, _defineProperty(_defineProperty({}, _useScroll.dataIndexAttribute, index), "index", index)));
},
placeholderRenderer: function placeholderRenderer(primary) {
return _placeholderRenderer({
handlePlaceholderFocus: handlePlaceholderFocus,
primary: primary
});
},
onUpdateItems: handleRestoreLastFocus,
updateStatesAndBounds: updateStatesAndBounds
});
};
/* eslint-disable enact/prop-types */
function _placeholderRenderer(_ref4) {
var handlePlaceholderFocus = _ref4.handlePlaceholderFocus,
primary = _ref4.primary;
return primary ? null : /*#__PURE__*/(0, _jsxRuntime.jsx)(SpotlightPlaceholder, {
"data-index": 0,
"data-vl-placeholder": true,
// a zero width/height element can't be focused by spotlight so we're giving
// the placeholder a small size to ensure it is navigable
onFocus: handlePlaceholderFocus,
style: {
width: 10
}
}, "placeholder");
}
/* eslint-enable enact/prop-types */
var _default = exports["default"] = useThemeVirtualList;