UNPKG

@enact/sandstone

Version:

Large-screen/TV support library for Enact, containing a variety of UI components.

475 lines (456 loc) 23.2 kB
"use strict"; 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;