UNPKG

@enact/sandstone

Version:

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

410 lines (390 loc) 19.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useThemeScroller = exports["default"] = void 0; var _handle = require("@enact/core/handle"); var _keymap = require("@enact/core/keymap"); var _platform = _interopRequireDefault(require("@enact/core/platform")); var _spotlight = _interopRequireDefault(require("@enact/spotlight")); var _utils = require("@enact/spotlight/src/utils"); var _resolution = _interopRequireDefault(require("@enact/ui/resolution")); var _utilDOM = _interopRequireDefault(require("@enact/ui/useScroll/utilDOM")); var _classnames = _interopRequireDefault(require("classnames")); var _react = require("react"); var _useScroll = require("../useScroll"); var _useEvent = require("./useEvent"); var _ScrollerModule = _interopRequireDefault(require("./Scroller.module.css")); var _ScrollbarTrackModule = _interopRequireDefault(require("../useScroll/ScrollbarTrack.module.css")); var _excluded = ["className", "children", "editable", "fadeOut", "scrollContainerRef"], _excluded2 = ["setNavigableFilter"]; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 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); } 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; } var isCancel = (0, _keymap.is)('cancel'), isEnter = (0, _keymap.is)('enter'), isBody = function isBody(elem) { return elem.classList.contains(_ScrollerModule["default"].focusableBody); }; var getFocusableBodyProps = function getFocusableBodyProps(scrollContainerRef, contentId, isScrollbarVisible) { var spotlightId = scrollContainerRef.current && scrollContainerRef.current.dataset.spotlightId; var consumeKeyUpTarget = null; var setNavigableFilter = function setNavigableFilter(_ref) { var filterTarget = _ref.filterTarget; spotlightId = scrollContainerRef.current && scrollContainerRef.current.dataset.spotlightId; if (spotlightId && filterTarget) { var targetClassName = filterTarget === 'body' ? _ScrollerModule["default"].focusableBody : _ScrollbarTrackModule["default"].thumb; _spotlight["default"].set(spotlightId, { navigableFilter: function navigableFilter(elem) { return typeof elem === 'string' || !elem.classList.contains(targetClassName); } }); return true; } return false; }; var getNavigableFilterTarget = function getNavigableFilterTarget(ev) { var keyCode = ev.keyCode, target = ev.target, type = ev.type; var filterTarget = null; if (!isScrollbarVisible) { return { filterTarget: filterTarget }; } if (type === 'focus') { filterTarget = isBody(target) ? 'thumb' : 'body'; } else if (type === 'blur') { filterTarget = 'body'; } else if (type === 'keydown') { filterTarget = !_spotlight["default"].getPointerMode() && isEnter(keyCode) && isBody(target) && 'body' || isEnter(keyCode) && !isBody(target) && 'thumb' || isCancel(keyCode) && !isBody(target) && 'thumb' || null; } return { filterTarget: filterTarget }; }; var consumeEventKeyDownWithFocus = function consumeEventKeyDownWithFocus(ev) { var keyCode = ev.keyCode, target = ev.target; var nextTarget; if (isBody(target)) { // Enter key on scroll Body. // Scroll thumb get focus. var spottableDescendants = _spotlight["default"].getSpottableDescendants(spotlightId); if (spottableDescendants.length > 0) { // Last spottable descendant(thumb) get focus. nextTarget = spottableDescendants.pop(); // If there are both thumbs, vertical thumb is the next target var verticalThumb = spottableDescendants.pop(); nextTarget = verticalThumb && verticalThumb.classList.contains(_ScrollbarTrackModule["default"].thumb) ? verticalThumb : nextTarget; } } else { // Enter or Cancel key on scroll thumb. // Scroll body get focus. nextTarget = target.closest(".".concat(_ScrollerModule["default"].focusableBody)); consumeKeyUpTarget = isCancel(keyCode) && nextTarget || null; } if (nextTarget) { _spotlight["default"].focus(nextTarget); ev.preventDefault(); ev.nativeEvent.stopImmediatePropagation(); } }; var consumeEventKeyUp = function consumeEventKeyUp(ev) { var keyCode = ev.keyCode, target = ev.target; if (isCancel(keyCode) && target === consumeKeyUpTarget) { ev.nativeEvent.stopImmediatePropagation(); } consumeKeyUpTarget = null; }; return { 'aria-labelledby': contentId, className: _ScrollerModule["default"].focusableBody, onFocus: (0, _handle.handle)((0, _handle.forward)('onFocus'), (0, _handle.adaptEvent)(getNavigableFilterTarget, setNavigableFilter)), onBlur: (0, _handle.handle)( // Focus out to external element. (0, _handle.forward)('onBlur'), (0, _handle.adaptEvent)(getNavigableFilterTarget, setNavigableFilter)), onKeyDown: (0, _handle.handle)((0, _handle.forward)('onKeyDown'), (0, _handle.adaptEvent)(getNavigableFilterTarget, setNavigableFilter), consumeEventKeyDownWithFocus), onKeyUp: (0, _handle.handle)(consumeEventKeyUp), setNavigableFilter: setNavigableFilter }; }; var useSpottable = function useSpottable(props, instances) { var scrollContainerRef = instances.scrollContainerRef, scrollContentHandle = instances.scrollContentHandle, scrollContentRef = instances.scrollContentRef; // Hooks var _useEventKey = (0, _useEvent.useEventKey)(), addGlobalKeyDownEventListener = _useEventKey.addGlobalKeyDownEventListener, removeGlobalKeyDownEventListener = _useEventKey.removeGlobalKeyDownEventListener; var setContainerDisabled = (0, _react.useCallback)(function (bool) { if (scrollContainerRef.current) { scrollContainerRef.current.dataset.spotlightContainerDisabled = bool; if (bool) { addGlobalKeyDownEventListener(function () { setContainerDisabled(false); }); } else { removeGlobalKeyDownEventListener(); } } }, [addGlobalKeyDownEventListener, removeGlobalKeyDownEventListener, scrollContainerRef]); (0, _react.useEffect)(function () { return function () { return setContainerDisabled(false); }; }, []); // eslint-disable-line react-hooks/exhaustive-deps (0, _react.useEffect)(function () { var onUpdate = props.onUpdate; if (onUpdate) { onUpdate(); } }); // Functions /** * Returns the first spotlight container between `node` and the scroller * * @param {Node} node A DOM node * * @returns {Node|Null} Spotlight container for `node` * @private */ function getSpotlightContainerForNode(node) { do { if (node.dataset.spotlightId && node.dataset.spotlightContainer && !node.dataset.expandableContainer) { return node; } } while ((node = node.parentNode) && node !== scrollContentRef.current); } /** * Calculates the "focus bounds" of a node. If the node is within a spotlight container, that * container is scrolled into view rather than just the element. * * @param {Node} node Focused node * * @returns {Object} Bounds as returned by `getBoundingClientRect` * @private */ function getFocusedItemBounds(node) { node = getSpotlightContainerForNode(node) || node; return node.getBoundingClientRect(); } /** * Calculates the new `scrollTop`. * * @param {Node} item Focused item node * * @returns {Number} Calculated `scrollTop` * @private */ function calculateScrollTop(item) { var threshold = _resolution["default"].scale(48); var roundToStart = function roundToStart(sb, st) { // round to start if (st < threshold) return 0; return st; }; var roundToEnd = function roundToEnd(sb, st, sh) { // round to end if (sh - (st + sb.height) < threshold) return sh - sb.height; return st + _resolution["default"].scale(_useScroll.affordanceSize); }; // adding threshold into these determinations ensures that items that are within that are // near the bounds of the scroller cause the edge to be scrolled into view even when the // item itself is in view (e.g. due to margins) var isItemBeforeView = function isItemBeforeView(ib, sb, d) { return ib.top + d - threshold < sb.top; }; var isItemAfterView = function isItemAfterView(ib, sb, d) { return ib.top + d + ib.height + threshold > sb.top + sb.height; }; var canItemFit = function canItemFit(ib, sb) { return ib.height <= sb.height; }; var calcItemAtStart = function calcItemAtStart(ib, sb, st, d) { return ib.top + st + d - sb.top; }; var calcItemAtEnd = function calcItemAtEnd(ib, sb, st, d) { return ib.top + ib.height + st + d - (sb.top + sb.height); }; var calcItemInView = function calcItemInView(ib, sb, st, sh, d) { if (isItemBeforeView(ib, sb, d)) { return roundToStart(sb, calcItemAtStart(ib, sb, st, d)); } else if (isItemAfterView(ib, sb, d)) { return roundToEnd(sb, calcItemAtEnd(ib, sb, st, d), sh); } return st; }; var container = getSpotlightContainerForNode(item); var scrollerBounds = scrollContentRef.current.getBoundingClientRect(); var _scrollContentRef$cur = scrollContentRef.current, scrollHeight = _scrollContentRef$cur.scrollHeight, scrollTop = _scrollContentRef$cur.scrollTop; var scrollTopDelta = 0; var adjustScrollTop = function adjustScrollTop(v) { scrollTopDelta = scrollTop - v; scrollTop = v; }; if (container) { var containerBounds = container.getBoundingClientRect(); // if the entire container fits in the scroller, scroll it into view if (canItemFit(containerBounds, scrollerBounds)) { return calcItemInView(containerBounds, scrollerBounds, scrollTop, scrollHeight, scrollTopDelta); } // if the container doesn't fit, adjust the scroll top ... if (containerBounds.top > scrollerBounds.top) { // ... to the top of the container if the top is below the top of the scroller adjustScrollTop(calcItemAtStart(containerBounds, scrollerBounds, scrollTop, scrollTopDelta)); } // removing support for "snap to bottom" for 2.2.8 // } else if (containerBounds.top + containerBounds.height < scrollerBounds.top + scrollerBounds.height) { // // ... to the bottom of the container if the bottom is above the bottom of the // // scroller // adjustScrollTop(calcItemAtEnd(containerBounds, scrollerBounds, scrollTop, scrollTopDelta)); // } // N.B. if the container covers the scrollable area (its top is above the top of the // scroller and its bottom is below the bottom of the scroller), we need not adjust the // scroller to ensure the container is wholly in view. } var itemBounds = item.getBoundingClientRect(); return calcItemInView(itemBounds, scrollerBounds, scrollTop, scrollHeight, scrollTopDelta); } /** * Calculates the new `scrollLeft`. * * @param {Node} item node * @param {Number} scrollPosition last target position, passed when scroll animation is ongoing * * @returns {Number} Calculated `scrollLeft` * @private */ function calculateScrollLeft(item, scrollPosition) { var scrollContentNode = scrollContentRef.current; var _getFocusedItemBounds = getFocusedItemBounds(item), itemLeft = _getFocusedItemBounds.left, itemWidth = _getFocusedItemBounds.width; var rtl = props.rtl, coordinateCoefficient = rtl && !(_platform["default"].chrome < 85) ? -1 : 1, clientWidth = scrollContentHandle.current.scrollBounds.clientWidth, rtlDirection = rtl ? -1 : 1, _scrollContentNode$ge = scrollContentNode.getBoundingClientRect(), containerLeft = _scrollContentNode$ge.left, scrollLastPosition = scrollPosition ? scrollPosition : scrollContentHandle.current.scrollPos.left, currentScrollLeft = rtl && coordinateCoefficient === 1 ? scrollContentHandle.current.scrollBounds.maxLeft - scrollLastPosition : scrollLastPosition, newItemLeft = coordinateCoefficient * scrollContentNode.scrollLeft + (itemLeft - containerLeft); var nextScrollLeft = scrollContentHandle.current.scrollPos.left; if (newItemLeft + itemWidth > clientWidth + currentScrollLeft && itemWidth < clientWidth) { // If focus is moved to an element outside of view area (to the right), scroller will move // to the right just enough to show the current `focusedItem`. This does not apply to // `focusedItem` that has a width that is bigger than `scrollBounds.clientWidth`. nextScrollLeft += rtlDirection * (newItemLeft + itemWidth - (clientWidth + currentScrollLeft)); } else if (newItemLeft < currentScrollLeft) { // If focus is outside of the view area to the left, move scroller to the left accordingly. nextScrollLeft += rtlDirection * (newItemLeft - currentScrollLeft); } return nextScrollLeft; } /** * Calculates the new top and left position for scroller based on focusedItem. * * @param {Node} item node * @param {Number} scrollPosition last target position, passed scroll animation is ongoing * * @returns {Object} with keys {top, left} containing calculated top and left positions for scroll. * @private */ function calculatePositionOnFocus(_ref2) { var item = _ref2.item, scrollPosition = _ref2.scrollPosition; var containerNode = scrollContentRef.current; var horizontal = scrollContentHandle.current.isHorizontal(); var vertical = scrollContentHandle.current.isVertical(); if (!vertical && !horizontal || !item || !_utilDOM["default"].containsDangerously(containerNode, item)) { return; } var containerRect = (0, _utils.getRect)(containerNode); var itemRect = (0, _utils.getRect)(item); if (horizontal && !(itemRect.left >= containerRect.left + _resolution["default"].scale(_useScroll.affordanceSize) && itemRect.right <= containerRect.right - _resolution["default"].scale(_useScroll.affordanceSize))) { scrollContentHandle.current.scrollPos.left = calculateScrollLeft(item, scrollPosition); } if (vertical && !(itemRect.top >= containerRect.top && itemRect.bottom <= containerRect.bottom - _resolution["default"].scale(_useScroll.affordanceSize))) { scrollContentHandle.current.scrollPos.top = calculateScrollTop(item); } return scrollContentHandle.current.scrollPos; } function focusOnNode(node) { if (node) { _spotlight["default"].focus(node); } } // Return return { calculatePositionOnFocus: calculatePositionOnFocus, focusOnNode: focusOnNode, setContainerDisabled: setContainerDisabled }; }; var useThemeScroller = exports.useThemeScroller = function useThemeScroller(props, scrollContentProps, contentId, isHorizontalScrollbarVisible, isVerticalScrollbarVisible) { var className = scrollContentProps.className, children = scrollContentProps.children, editable = scrollContentProps.editable, fadeOut = scrollContentProps.fadeOut, scrollContainerRef = scrollContentProps.scrollContainerRef, rest = _objectWithoutProperties(scrollContentProps, _excluded); var scrollContainerHandle = rest.scrollContainerHandle, scrollContentHandle = rest.scrollContentHandle, scrollContentRef = rest.scrollContentRef; delete rest.onUpdate; delete rest.scrollContainerContainsDangerously; delete rest.scrollContainerHandle; delete rest.scrollContentHandle; delete rest.setThemeScrollContentHandle; delete rest.spotlightId; // Hooks var isScrollbarVisible = isHorizontalScrollbarVisible || isVerticalScrollbarVisible; var _useSpottable = useSpottable(scrollContentProps, { scrollContainerRef: scrollContainerRef, scrollContentHandle: scrollContentHandle, scrollContentRef: scrollContentRef }), calculatePositionOnFocus = _useSpottable.calculatePositionOnFocus, focusOnNode = _useSpottable.focusOnNode, setContainerDisabled = _useSpottable.setContainerDisabled; var _ref3 = props.focusableScrollbar === 'byEnter' ? getFocusableBodyProps(scrollContainerRef, contentId, isScrollbarVisible) : {}, setNavigableFilter = _ref3.setNavigableFilter, focusableBodyProps = _objectWithoutProperties(_ref3, _excluded2); (0, _react.useLayoutEffect)(function () { // Initial filter setting if (setNavigableFilter) { setNavigableFilter({ filterTarget: 'body' }); } }, [props.focusableScrollbar, scrollContainerRef.current]); // eslint-disable-line react-hooks/exhaustive-deps scrollContentProps.setThemeScrollContentHandle({ calculatePositionOnFocus: calculatePositionOnFocus, focusOnNode: focusOnNode, setContainerDisabled: setContainerDisabled }); // Render rest.className = (0, _classnames["default"])(className, !isHorizontalScrollbarVisible && isVerticalScrollbarVisible && fadeOut ? _ScrollerModule["default"].verticalFadeout : null, isHorizontalScrollbarVisible && !isVerticalScrollbarVisible && fadeOut ? _ScrollerModule["default"].horizontalFadeout : null); return { editableWrapperProps: { children: children, editable: editable, scrollContainerHandle: scrollContainerHandle, scrollContainerRef: scrollContainerRef, scrollContentRef: scrollContentRef }, focusableBodyProps: focusableBodyProps, themeScrollContentProps: _objectSpread({}, rest) }; }; var _default = exports["default"] = useThemeScroller;