@enact/sandstone
Version:
Large-screen/TV support library for Enact, containing a variety of UI components.
410 lines (390 loc) • 19.7 kB
JavaScript
;
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;