From 5f97d1df84b7e67b3696cbedfd25545e0358a3a8 Mon Sep 17 00:00:00 2001 From: alexandru-morariu-lgp Date: Mon, 10 Feb 2025 17:30:29 +0200 Subject: [PATCH 1/5] Start point --- .../ContextualPopupDecorator.js | 1848 ++++++++++++----- 1 file changed, 1294 insertions(+), 554 deletions(-) diff --git a/ContextualPopupDecorator/ContextualPopupDecorator.js b/ContextualPopupDecorator/ContextualPopupDecorator.js index 8158812be..e8ddea639 100644 --- a/ContextualPopupDecorator/ContextualPopupDecorator.js +++ b/ContextualPopupDecorator/ContextualPopupDecorator.js @@ -22,7 +22,7 @@ import FloatingLayer from '@enact/ui/FloatingLayer'; import ri from '@enact/ui/resolution'; import compose from 'ramda/src/compose'; import PropTypes from 'prop-types'; -import {Component, createRef} from 'react'; +import {Component, createRef, useEffect, useRef, useState} from 'react'; import {ContextualPopup} from './ContextualPopup'; import HolePunchScrim from './HolePunchScrim'; @@ -82,302 +82,980 @@ const ContextualPopupContainer = SpotlightContainerDecorator( ContextualPopup ); -const Decorator = hoc(defaultConfig, (config, Wrapped) => { +const generateId = () => { + return Math.random().toString(36).substring(2, 10); +}; + +// const Decorator = hoc(defaultConfig, (config, Wrapped) => { +// const {noArrow, noSkin, openProp} = config; +// const WrappedWithRef = WithRef(Wrapped); +// +// return class extends Component { +// static displayName = 'ContextualPopupDecorator'; +// +// static propTypes = /** @lends limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype */ { +// /** +// * The component rendered within the +// * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup}. +// * +// * @type {Component} +// * @required +// * @public +// */ +// popupComponent: EnactPropTypes.component.isRequired, +// +// /** +// * Limits the range of voice control to the popup. +// * +// * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype +// * @type {Boolean} +// * @default true +// * @public +// */ +// 'data-webos-voice-exclusive': PropTypes.bool, +// +// /** +// * Direction of popup with respect to the wrapped component. +// * +// * @type {('above'|'above center'|'above left'|'above right'|'below'|'below center'|'below left'|'below right'|'left middle'|'left top'|'left bottom'|'right middle'|'right top'|'right bottom')} +// * @default 'below center' +// * @public +// */ +// direction: PropTypes.oneOf(['above', 'above center', 'above left', 'above right', 'below', 'below center', 'below left', 'below right', 'left middle', 'left top', 'left bottom', 'right middle', 'right top', 'right bottom']), +// +// /** +// * Disables closing the popup when the user presses the cancel/back (e.g. `ESC`) key or taps outside the +// * popup. +// * +// * @type {Boolean} +// * @default false +// * @public +// */ +// noAutoDismiss: PropTypes.bool, +// +// /** +// * Offset from the activator to apply to the position of the popup. +// * +// * Only applies when `noArrow` is `true`. +// * +// * @type {('none'|'overlap'|'small')} +// * @default 'small' +// * @public +// */ +// offset: PropTypes.oneOf(['none', 'overlap', 'small']), +// +// /** +// * Called when the user has attempted to close the popup. +// * +// * This may occur either when the close button is clicked or when spotlight focus +// * moves outside the boundary of the popup. Setting `spotlightRestrict` to `'self-only'` +// * will prevent Spotlight focus from leaving the popup. +// * +// * @type {Function} +// * @public +// */ +// onClose: PropTypes.func, +// +// /** +// * Called when the popup is opened. +// * +// * @type {Function} +// * @public +// */ +// onOpen: PropTypes.func, +// +// /** +// * Displays the contextual popup. +// * +// * @type {Boolean} +// * @default false +// * @public +// */ +// open: PropTypes.bool, +// +// /** +// * CSS class name to pass to the +// * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup}. +// * +// * This is commonly used to set width and height of the popup. +// * +// * @type {String} +// * @public +// */ +// popupClassName: PropTypes.string, +// +// /** +// * An object containing properties to be passed to popup component. +// * +// * @type {Object} +// * @public +// */ +// popupProps: PropTypes.object, +// +// /** +// * The container ID to use with Spotlight. +// * +// * The spotlight container for the popup isn't created until it is open. To configure +// * the container using `Spotlight.set()`, handle the `onOpen` event which is fired after +// * the popup has been created and opened. +// * +// * @type {String} +// * @public +// */ +// popupSpotlightId: PropTypes.string, +// +// /** +// * Indicates the content's text direction is right-to-left. +// * +// * @type {Boolean} +// * @private +// */ +// rtl: PropTypes.bool, +// +// /** +// * Set the type of scrim to use +// * +// * @type {('holepunch'|'translucent'|'transparent'|'none')} +// * @default 'none' +// * @private +// */ +// scrimType: PropTypes.oneOf(['holepunch', 'translucent', 'transparent', 'none']), +// +// /** +// * Registers the ContextualPopupDecorator component with an +// * {@link core/internal/ApiDecorator.ApiDecorator|ApiDecorator}. +// * +// * @type {Function} +// * @private +// */ +// setApiProvider: PropTypes.func, +// +// /** +// * The current skin for this component. +// * +// * When `noSkin` is set on the config object, `skin` will only be applied to the +// * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup} and not +// * to the popup's activator component. +// * +// * @see {@link limestone/Skinnable.Skinnable.skin} +// * @type {String} +// * @public +// */ +// skin: PropTypes.string, +// +// /** +// * Restricts or prioritizes spotlight navigation. +// * +// * Allowed values are: +// * * `'none'` - Spotlight can move freely within and beyond the popup +// * * `'self-first'` - Spotlight should prefer components within the popup over +// * components beyond the popup, or +// * * `'self-only'` - Spotlight can only be set within the popup +// * +// * @type {('none'|'self-first'|'self-only')} +// * @default 'self-first' +// * @public +// */ +// spotlightRestrict: PropTypes.oneOf(['none', 'self-first', 'self-only']) +// }; +// +// static defaultProps = { +// 'data-webos-voice-exclusive': true, +// direction: 'below center', +// noAutoDismiss: false, +// offset: 'small', +// open: false, +// scrimType: 'none', +// spotlightRestrict: 'self-first' +// }; +// +// constructor (props) { +// super(props); +// this.state = { +// arrowPosition: {top: 0, left: 0}, +// containerPosition: {top: 0, left: 0, right: 0}, +// containerId: Spotlight.add(this.props.popupSpotlightId), +// activator: null +// }; +// +// this.resizeObserver = null; +// this.overflow = {}; +// this.adjustedDirection = this.props.direction; +// this.id = this.generateId(); +// this.clientSiblingRef = createRef(null); +// +// this.MARGIN = ri.scale(noArrow ? 0 : 12); +// this.ARROW_WIDTH = noArrow ? 0 : ri.scale(60); // svg arrow width. used for arrow positioning +// this.ARROW_OFFSET = noArrow ? 0 : ri.scale(36); // actual distance of the svg arrow displayed to offset overlaps with the container. Offset is when `noArrow` is false. +// this.KEEPOUT = ri.scale(24); // keep out distance on the edge of the screen +// +// if (props.setApiProvider) { +// props.setApiProvider(this); +// } +// } +// +// componentDidMount () { +// if (this.props.open) { +// on('keydown', this.handleKeyDown); +// on('keyup', this.handleKeyUp); +// } +// +// if (typeof ResizeObserver === 'function') { +// this.resizeObserver = new ResizeObserver(() => { +// this.positionContextualPopup(); +// }); +// } +// +// if (typeof MutationObserver === 'function') { +// this.mutationObserver = new MutationObserver(() => { +// this.positionContextualPopup(); +// }); +// } +// } +// +// getSnapshotBeforeUpdate (prevProps, prevState) { +// const snapshot = { +// containerWidth: this.getContainerNodeWidth() +// }; +// +// if (prevProps.open && !this.props.open) { +// const current = Spotlight.getCurrent(); +// snapshot.shouldSpotActivator = ( +// // isn't set +// !current || +// // is on the activator, and we want to re-spot it so a11y read out can occur +// current === prevState.activator || +// // is within the popup +// this.containerNode.contains(current) +// ); +// } +// +// return snapshot; +// } +// +// componentDidUpdate (prevProps, prevState, snapshot) { +// if (prevProps.direction !== this.props.direction || +// snapshot.containerWidth !== this.getContainerNodeWidth() || +// (prevProps.open && this.props.open)) { +// this.adjustedDirection = this.props.direction; +// // NOTE: `setState` is called and will cause re-render +// this.positionContextualPopup(); +// } +// +// if (this.props.open && !prevProps.open) { +// on('keydown', this.handleKeyDown); +// on('keyup', this.handleKeyUp); +// } else if (!this.props.open && prevProps.open) { +// off('keydown', this.handleKeyDown); +// off('keyup', this.handleKeyUp); +// if (snapshot && snapshot.shouldSpotActivator) { +// this.spotActivator(prevState.activator); +// } +// } +// } +// +// componentWillUnmount () { +// if (this.props.open) { +// off('keydown', this.handleKeyDown); +// off('keyup', this.handleKeyUp); +// } +// Spotlight.remove(this.state.containerId); +// +// if (this.resizeObserver) { +// this.resizeObserver.disconnect(); +// this.resizeObserver = null; +// } +// +// if (this.mutationObserver) { +// this.mutationObserver.disconnect(); +// this.mutationObserver = null; +// } +// } +// +// generateId = () => { +// return Math.random().toString(36).substring(2, 10); +// }; +// +// getContainerNodeWidth () { +// return this.containerNode && this.containerNode.getBoundingClientRect().width || 0; +// } +// +// updateLeaveFor (activator) { +// Spotlight.set(this.state.containerId, { +// leaveFor: { +// up: activator, +// down: activator, +// left: activator, +// right: activator +// } +// }); +// } +// +// getContainerAdjustedPosition = () => { +// const position = this.adjustedDirection; +// const arr = this.adjustedDirection.split(' '); +// let direction = null; +// let anchor = null; +// +// if (arr.length === 2) { +// [direction, anchor] = arr; +// } else { +// direction = position; +// } +// +// return {anchor, direction}; +// }; +// +// getContainerPosition (containerNode, clientNode) { +// const position = this.centerContainerPosition(containerNode, clientNode); +// const {direction} = this.getContainerAdjustedPosition(); +// +// switch (direction) { +// case 'above': +// position.top = clientNode.top - this.ARROW_OFFSET - containerNode.height - this.MARGIN; +// break; +// case 'below': +// position.top = clientNode.bottom + this.ARROW_OFFSET + this.MARGIN; +// break; +// case 'right': +// position.left = this.props.rtl ? clientNode.left - containerNode.width - this.ARROW_OFFSET - this.MARGIN : clientNode.right + this.ARROW_OFFSET + this.MARGIN; +// break; +// case 'left': +// position.left = this.props.rtl ? clientNode.right + this.ARROW_OFFSET + this.MARGIN : clientNode.left - containerNode.width - this.ARROW_OFFSET - this.MARGIN; +// break; +// } +// +// return this.adjustRTL(position); +// } +// +// centerContainerPosition (containerNode, clientNode) { +// const pos = {}; +// const {anchor, direction} = this.getContainerAdjustedPosition(); +// +// if (direction === 'above' || direction === 'below') { +// if (this.overflow.isOverLeft) { +// // anchor to the left of the screen +// pos.left = this.KEEPOUT; +// } else if (this.overflow.isOverRight) { +// // anchor to the right of the screen +// pos.left = window.innerWidth - containerNode.width - this.KEEPOUT; +// } else if (anchor) { +// if (anchor === 'center') { +// // center horizontally +// pos.left = clientNode.left + (clientNode.width - containerNode.width) / 2; +// } else if (anchor === 'left') { +// // anchor to the left side of the activator +// pos.left = clientNode.left; +// } else { +// // anchor to the right side of the activator +// pos.left = clientNode.right - containerNode.width; +// } +// } else { +// // anchor to the left side of the activator, matching its width +// pos.left = clientNode.left; +// pos.width = clientNode.width; +// } +// +// } else if (direction === 'left' || direction === 'right') { +// if (this.overflow.isOverTop) { +// // anchor to the top of the screen +// pos.top = this.KEEPOUT; +// } else if (this.overflow.isOverBottom) { +// // anchor to the bottom of the screen +// pos.top = window.innerHeight - containerNode.height - this.KEEPOUT; +// } else if (anchor === 'middle') { +// // center vertically +// pos.top = clientNode.top - (containerNode.height - clientNode.height) / 2; +// } else if (anchor === 'top') { +// // anchor to the top of the activator +// pos.top = clientNode.top; +// } else { +// // anchor to the bottom of the activator +// pos.top = clientNode.bottom - containerNode.height; +// } +// } +// +// return pos; +// } +// +// getArrowPosition (containerNode, clientNode) { +// const position = {}; +// const {anchor, direction} = this.getContainerAdjustedPosition(); +// +// if (direction === 'above' || direction === 'below') { +// if (this.overflow.isOverRight && !this.overflow.isOverLeft) { +// position.left = window.innerWidth - ((containerNode.width + this.ARROW_WIDTH) / 2) - this.KEEPOUT; +// } else if (!this.overflow.isOverRight && this.overflow.isOverLeft) { +// position.left = ((containerNode.width - this.ARROW_WIDTH) / 2) + this.KEEPOUT; +// } else if (anchor === 'left') { +// position.left = clientNode.left + (containerNode.width - this.ARROW_WIDTH) / 2; +// } else if (anchor === 'right') { +// position.left = clientNode.right - containerNode.width + (containerNode.width - this.ARROW_WIDTH) / 2; +// } else { +// position.left = clientNode.left + (clientNode.width - this.ARROW_WIDTH) / 2; +// } +// } else if (this.overflow.isOverBottom && !this.overflow.isOverTop) { +// position.top = window.innerHeight - ((containerNode.height + this.ARROW_WIDTH) / 2) - this.KEEPOUT; +// } else if (!this.overflow.isOverBottom && this.overflow.isOverTop) { +// position.top = ((containerNode.height - this.ARROW_WIDTH) / 2) + this.KEEPOUT; +// } else if (anchor === 'top') { +// position.top = clientNode.top + (containerNode.height - this.ARROW_WIDTH) / 2; +// } else if (anchor === 'bottom') { +// position.top = clientNode.bottom - containerNode.height + (containerNode.height - this.ARROW_WIDTH) / 2; +// } else { +// position.top = clientNode.top + (clientNode.height - this.ARROW_WIDTH) / 2; +// } +// +// switch (direction) { +// case 'above': +// position.top = clientNode.top - this.ARROW_WIDTH - this.MARGIN; +// break; +// case 'below': +// position.top = clientNode.bottom + this.MARGIN; +// break; +// case 'left': +// position.left = this.props.rtl ? clientNode.left + clientNode.width + this.MARGIN : clientNode.left - this.ARROW_WIDTH - this.MARGIN; +// break; +// case 'right': +// position.left = this.props.rtl ? clientNode.left - this.ARROW_WIDTH - this.MARGIN : clientNode.left + clientNode.width + this.MARGIN; +// break; +// default: +// return {}; +// } +// +// return this.adjustRTL(position); +// } +// +// calcOverflow (container, client) { +// let containerHeight, containerWidth; +// const {anchor, direction} = this.getContainerAdjustedPosition(); +// +// if (direction === 'above' || direction === 'below') { +// containerHeight = container.height; +// containerWidth = (container.width - client.width) / 2; +// } else { +// containerHeight = (container.height - client.height) / 2; +// containerWidth = container.width; +// } +// +// this.overflow = { +// isOverTop: anchor === 'top' && (direction === 'left' || direction === 'right') ? +// !(client.top > this.KEEPOUT) : +// client.top - containerHeight - this.ARROW_OFFSET - this.MARGIN - this.KEEPOUT < 0, +// isOverBottom: anchor === 'bottom' && (direction === 'left' || direction === 'right') ? +// client.bottom + this.KEEPOUT > window.innerHeight : +// client.bottom + containerHeight + this.ARROW_OFFSET + this.MARGIN + this.KEEPOUT > window.innerHeight, +// isOverLeft: anchor === 'left' && (direction === 'above' || direction === 'below') ? +// !(client.left > this.KEEPOUT) : +// client.left - containerWidth - this.ARROW_OFFSET - this.MARGIN - this.KEEPOUT < 0, +// isOverRight: anchor === 'right' && (direction === 'above' || direction === 'below') ? +// client.right + this.KEEPOUT > window.innerWidth : +// client.right + containerWidth + this.ARROW_OFFSET + this.MARGIN + this.KEEPOUT > window.innerWidth +// }; +// } +// +// adjustDirection () { +// const {anchor, direction} = this.getContainerAdjustedPosition(); +// +// if (this.overflow.isOverTop && !this.overflow.isOverBottom && direction === 'above') { +// this.adjustedDirection = anchor ? `below ${anchor}` : 'below'; +// } else if (this.overflow.isOverBottom && !this.overflow.isOverTop && direction === 'below') { +// this.adjustedDirection = anchor ? `above ${anchor}` : 'above'; +// } else if (this.overflow.isOverLeft && !this.overflow.isOverRight && direction === 'left' && !this.props.rtl) { +// this.adjustedDirection = anchor ? `right ${anchor}` : 'right'; +// } else if (this.overflow.isOverRight && !this.overflow.isOverLeft && direction === 'right' && !this.props.rtl) { +// this.adjustedDirection = anchor ? `left ${anchor}` : 'left'; +// } +// } +// +// adjustRTL (position) { +// let pos = position; +// if (this.props.rtl) { +// const tmpLeft = pos.left; +// pos.left = pos.right; +// pos.right = tmpLeft; +// } +// return pos; +// } +// +// /** +// * Position the popup in relation to the activator. +// * +// * Position is based on the dimensions of the popup and its activator. If the popup does not +// * fit in the specified direction, it will automatically flip to the opposite direction. +// * +// * @method +// * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype +// * @public +// * @returns {undefined} +// */ +// positionContextualPopup = () => { +// if (this.containerNode && this.clientSiblingRef?.current) { +// const containerNode = this.containerNode.getBoundingClientRect(); +// const {top, left, bottom, right, width, height} = this.clientSiblingRef.current.getBoundingClientRect(); +// const clientNode = {top, left, bottom, right, width, height}; +// +// clientNode.left = this.props.rtl ? window.innerWidth - right : left; +// clientNode.right = this.props.rtl ? window.innerWidth - left : right; +// +// this.calcOverflow(containerNode, clientNode); +// this.adjustDirection(); +// +// const arrowPosition = this.getArrowPosition(containerNode, clientNode), +// containerPosition = this.getContainerPosition(containerNode, clientNode); +// +// if ((this.state.direction !== this.adjustedDirection) || +// (this.state.arrowPosition.left !== arrowPosition.left) || +// (this.state.arrowPosition.top !== arrowPosition.top) || +// (this.state.containerPosition.left !== containerPosition.left) || +// (this.state.containerPosition.right !== containerPosition.right) || +// (this.state.containerPosition.top !== containerPosition.top) +// ) { +// this.setState({ +// direction: this.adjustedDirection, +// arrowPosition, +// containerPosition +// }); +// } +// } +// }; +// +// getContainerNode = (node) => { +// this.containerNode = node; +// +// if (this.resizeObserver) { +// if (node) { +// // It is not easy to trigger changed position of activator, +// // so we chose to observe the `div` element's size that has the real size below the root of floatLayer. +// // This implementation is dependent on the current structure of FloatingLayer, +// // so if the structure have changed, below code needs to be changed accordingly. +// this.resizeObserver.observe(node?.parentElement?.parentElement); +// } else { +// this.resizeObserver.disconnect(); +// } +// } +// +// if (this.mutationObserver) { +// if (node) { +// this.mutationObserver.observe(document.body, {attributes: false, childList: true, subtree: true}); +// } else { +// this.mutationObserver.disconnect(); +// } +// } +// }; +// +// handle = handle.bind(this); +// +// handleKeyUp = this.handle( +// forProp('open', true), +// forKey('enter'), +// () => Spotlight.getCurrent() === this.state.activator, +// stop, +// forwardCustom('onClose') +// ); +// +// handleOpen = (ev) => { +// forward('onOpen', ev, this.props); +// this.positionContextualPopup(); +// const current = Spotlight.getCurrent(); +// this.updateLeaveFor(current); +// this.setState({ +// activator: current +// }); +// this.spotPopupContent(); +// }; +// +// handleClose = () => { +// this.updateLeaveFor(null); +// this.setState({ +// activator: null +// }); +// }; +// +// handleDismiss = () => { +// forwardCustom('onClose')(null, this.props); +// }; +// +// handleDirectionalKey (ev) { +// // prevent default page scrolling +// ev.preventDefault(); +// // stop propagation to prevent default spotlight behavior +// ev.stopPropagation(); +// // set the pointer mode to false on keydown +// Spotlight.setPointerMode(false); +// } +// +// // handle key event from outside (i.e. the activator) to the popup container +// handleKeyDown = (ev) => { +// const {activator, containerId} = this.state; +// const {spotlightRestrict} = this.props; +// const current = Spotlight.getCurrent(); +// const direction = getDirection(ev.keyCode); +// +// if (!direction) return; +// +// const hasSpottables = Spotlight.getSpottableDescendants(containerId).length > 0; +// const spotlessSpotlightModal = spotlightRestrict === 'self-only' && !hasSpottables; +// const shouldSpotPopup = current === activator && direction === PositionToDirection[this.adjustedDirection.split(' ')[0]] && hasSpottables; +// +// if (shouldSpotPopup || spotlessSpotlightModal) { +// this.handleDirectionalKey(ev); +// +// // we guard against attempting a focus change by verifying the case where a +// // spotlightModal popup contains no spottable components +// if (!spotlessSpotlightModal && shouldSpotPopup) { +// this.spotPopupContent(); +// } +// } +// }; +// +// // handle key event from contextual popup and closes the popup +// handleContainerKeyDown = (ev) => { +// // Note: Container will be only rendered if `open`ed, therefore no need to check for `open` +// const direction = getDirection(ev.keyCode); +// +// if (!direction) return; +// +// this.handleDirectionalKey(ev); +// +// // if focus moves outside the popup's container, issue the `onClose` event +// if (Spotlight.move(direction) && !this.containerNode.contains(Spotlight.getCurrent())) { +// forwardCustom('onClose')(null, this.props); +// } +// }; +// +// spotActivator = (activator) => { +// if (!Spotlight.getPointerMode() && activator && activator === Spotlight.getCurrent()) { +// activator.blur(); +// } +// if (!Spotlight.focus(activator)) { +// Spotlight.focus(); +// } +// }; +// +// spotPopupContent = () => { +// const {spotlightRestrict} = this.props; +// const {containerId} = this.state; +// const spottableDescendants = Spotlight.getSpottableDescendants(containerId); +// if (spotlightRestrict === 'self-only' && spottableDescendants.length && Spotlight.getCurrent()) { +// Spotlight.getCurrent().blur(); +// } +// +// if (!Spotlight.focus(containerId)) { +// Spotlight.setActiveContainer(containerId); +// } +// }; +// +// render () { +// const {'data-webos-voice-exclusive': voiceExclusive, popupComponent: PopupComponent, popupClassName, noAutoDismiss, open, offset, popupProps, skin, spotlightRestrict, ...rest} = this.props; +// const idFloatLayer = `${this.id}_floatLayer`; +// let scrimType = rest.scrimType; +// delete rest.scrimType; +// +// // 'holepunch' scrimType is specific to this component, not supported by floating layer +// // so it must be swapped-out for one that FloatingLayer does support. +// const holepunchScrim = (scrimType === 'holepunch'); +// if ((spotlightRestrict === 'self-only' && scrimType === 'none') || holepunchScrim) { +// scrimType = 'transparent'; +// } +// +// const popupPropsRef = Object.assign({}, popupProps); +// const ariaProps = extractAriaProps(popupPropsRef); +// +// if (!noSkin) { +// rest.skin = skin; +// } +// +// let holeBounds; +// if (this.clientSiblingRef?.current && holepunchScrim) { +// holeBounds = this.clientSiblingRef.current.getBoundingClientRect(); +// } +// +// delete rest.direction; +// delete rest.onClose; +// delete rest.onOpen; +// delete rest.popupSpotlightId; +// delete rest.rtl; +// delete rest.setApiProvider; +// +// if (openProp) rest[openProp] = open; +// +// return ( +//
+// +//
+// {holepunchScrim ? : null} +// +// +// +//
+//
+// +//
+// ); +// } +// }; +// }); + +const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { const {noArrow, noSkin, openProp} = config; const WrappedWithRef = WithRef(Wrapped); - return class extends Component { - static displayName = 'ContextualPopupDecorator'; - - static propTypes = /** @lends limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype */ { - /** - * The component rendered within the - * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup}. - * - * @type {Component} - * @required - * @public - */ - popupComponent: EnactPropTypes.component.isRequired, - - /** - * Limits the range of voice control to the popup. - * - * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype - * @type {Boolean} - * @default true - * @public - */ - 'data-webos-voice-exclusive': PropTypes.bool, - - /** - * Direction of popup with respect to the wrapped component. - * - * @type {('above'|'above center'|'above left'|'above right'|'below'|'below center'|'below left'|'below right'|'left middle'|'left top'|'left bottom'|'right middle'|'right top'|'right bottom')} - * @default 'below center' - * @public - */ - direction: PropTypes.oneOf(['above', 'above center', 'above left', 'above right', 'below', 'below center', 'below left', 'below right', 'left middle', 'left top', 'left bottom', 'right middle', 'right top', 'right bottom']), - - /** - * Disables closing the popup when the user presses the cancel/back (e.g. `ESC`) key or taps outside the - * popup. - * - * @type {Boolean} - * @default false - * @public - */ - noAutoDismiss: PropTypes.bool, - - /** - * Offset from the activator to apply to the position of the popup. - * - * Only applies when `noArrow` is `true`. - * - * @type {('none'|'overlap'|'small')} - * @default 'small' - * @public - */ - offset: PropTypes.oneOf(['none', 'overlap', 'small']), - - /** - * Called when the user has attempted to close the popup. - * - * This may occur either when the close button is clicked or when spotlight focus - * moves outside the boundary of the popup. Setting `spotlightRestrict` to `'self-only'` - * will prevent Spotlight focus from leaving the popup. - * - * @type {Function} - * @public - */ - onClose: PropTypes.func, - - /** - * Called when the popup is opened. - * - * @type {Function} - * @public - */ - onOpen: PropTypes.func, - - /** - * Displays the contextual popup. - * - * @type {Boolean} - * @default false - * @public - */ - open: PropTypes.bool, - - /** - * CSS class name to pass to the - * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup}. - * - * This is commonly used to set width and height of the popup. - * - * @type {String} - * @public - */ - popupClassName: PropTypes.string, - - /** - * An object containing properties to be passed to popup component. - * - * @type {Object} - * @public - */ - popupProps: PropTypes.object, - - /** - * The container ID to use with Spotlight. - * - * The spotlight container for the popup isn't created until it is open. To configure - * the container using `Spotlight.set()`, handle the `onOpen` event which is fired after - * the popup has been created and opened. - * - * @type {String} - * @public - */ - popupSpotlightId: PropTypes.string, - - /** - * Indicates the content's text direction is right-to-left. - * - * @type {Boolean} - * @private - */ - rtl: PropTypes.bool, - - /** - * Set the type of scrim to use - * - * @type {('holepunch'|'translucent'|'transparent'|'none')} - * @default 'none' - * @private - */ - scrimType: PropTypes.oneOf(['holepunch', 'translucent', 'transparent', 'none']), - - /** - * Registers the ContextualPopupDecorator component with an - * {@link core/internal/ApiDecorator.ApiDecorator|ApiDecorator}. - * - * @type {Function} - * @private - */ - setApiProvider: PropTypes.func, - - /** - * The current skin for this component. - * - * When `noSkin` is set on the config object, `skin` will only be applied to the - * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup} and not - * to the popup's activator component. - * - * @see {@link limestone/Skinnable.Skinnable.skin} - * @type {String} - * @public - */ - skin: PropTypes.string, - - /** - * Restricts or prioritizes spotlight navigation. - * - * Allowed values are: - * * `'none'` - Spotlight can move freely within and beyond the popup - * * `'self-first'` - Spotlight should prefer components within the popup over - * components beyond the popup, or - * * `'self-only'` - Spotlight can only be set within the popup - * - * @type {('none'|'self-first'|'self-only')} - * @default 'self-first' - * @public - */ - spotlightRestrict: PropTypes.oneOf(['none', 'self-first', 'self-only']) + const Decorator = ({ + 'data-webos-voice-exclusive': voiceExclusive = true, + direction = 'below center', + noAutoDismiss = false, + offset = 'small', + open = false, + scrimType = 'none', + spotlightRestrict = 'self-first', + ...props + }) => { + // this.state + const containerId = Spotlight.add(props.popupSpotlightId); + const [arrowPosition, setArrowPosition] = useState({top: 0, left: 0}); + const [containerPosition, setContainerPosition] = useState({top: 0, left: 0, right: 0}); + const [activator, setActivator] = useState(null); + const [stateDirection, setStateDirection] = useState(direction); // new + + // refs + const resizeObserver = useRef(null); + const mutationObserver = useRef(null); // new + const containerNode = useRef(null); // new + const overflow = useRef({}); + const adjustedDirection = useRef(direction); + const id = generateId(); + const clientSiblingRef = useRef(null); + + // allCaps variables + const MARGIN = ri.scale(noArrow ? 0 : 12); + const ARROW_WIDTH = noArrow ? 0 : ri.scale(60); // svg arrow width. used for arrow positioning + const ARROW_OFFSET = noArrow ? 0 : ri.scale(36); // actual distance of the svg arrow displayed to offset overlaps with the container. Offset is when `noArrow` is false. + const KEEPOUT = ri.scale(24); // keep out distance on the edge of the screen + + // needs investigating + if (props.setApiProvider) { + props.setApiProvider(this); + } + + const handleDirectionalKey = (ev) => { + // prevent default page scrolling + ev.preventDefault(); + // stop propagation to prevent default spotlight behavior + ev.stopPropagation(); + // set the pointer mode to false on keydown + Spotlight.setPointerMode(false); }; - static defaultProps = { - 'data-webos-voice-exclusive': true, - direction: 'below center', - noAutoDismiss: false, - offset: 'small', - open: false, - scrimType: 'none', - spotlightRestrict: 'self-first' + const spotPopupContent = () => { + const spottableDescendants = Spotlight.getSpottableDescendants(containerId); + if (spotlightRestrict === 'self-only' && spottableDescendants.length && Spotlight.getCurrent()) { + Spotlight.getCurrent().blur(); + } + + if (!Spotlight.focus(containerId)) { + Spotlight.setActiveContainer(containerId); + } }; - constructor (props) { - super(props); - this.state = { - arrowPosition: {top: 0, left: 0}, - containerPosition: {top: 0, left: 0, right: 0}, - containerId: Spotlight.add(this.props.popupSpotlightId), - activator: null - }; + // handle key event from outside (i.e. the activator) to the popup container + const handleKeyDown = (ev) => { + const current = Spotlight.getCurrent(); + const direction = getDirection(ev.keyCode); - this.resizeObserver = null; - this.overflow = {}; - this.adjustedDirection = this.props.direction; - this.id = this.generateId(); - this.clientSiblingRef = createRef(null); + if (!direction) return; - this.MARGIN = ri.scale(noArrow ? 0 : 12); - this.ARROW_WIDTH = noArrow ? 0 : ri.scale(60); // svg arrow width. used for arrow positioning - this.ARROW_OFFSET = noArrow ? 0 : ri.scale(36); // actual distance of the svg arrow displayed to offset overlaps with the container. Offset is when `noArrow` is false. - this.KEEPOUT = ri.scale(24); // keep out distance on the edge of the screen + const hasSpottables = Spotlight.getSpottableDescendants(containerId).length > 0; + const spotlessSpotlightModal = spotlightRestrict === 'self-only' && !hasSpottables; + const shouldSpotPopup = current === activator && direction === PositionToDirection[adjustedDirection.current.split(' ')[0]] && hasSpottables; + + if (shouldSpotPopup || spotlessSpotlightModal) { + handleDirectionalKey(ev); - if (props.setApiProvider) { - props.setApiProvider(this); + // we guard against attempting a focus change by verifying the case where a + // spotlightModal popup contains no spottable components + if (!spotlessSpotlightModal && shouldSpotPopup) { + spotPopupContent(); + } } - } + }; + + // unknown how to convert this + handle = handle.bind(this); + // unknown how to convert this + const handleKeyUp = handle( + forProp('open', true), + forKey('enter'), + () => Spotlight.getCurrent() === activator, + stop, + forwardCustom('onClose') // needs a method to pass onClose inside forward + ); + + + /** + * Position the popup in relation to the activator. + * + * Position is based on the dimensions of the popup and its activator. If the popup does not + * fit in the specified direction, it will automatically flip to the opposite direction. + * + * @method + * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype + * @public + * @returns {undefined} + */ + const positionContextualPopup = () => { + if (containerNode.current && clientSiblingRef?.current) { + const localContainerNode = containerNode.current.getBoundingClientRect(); + const {top, left, bottom, right, width, height} = clientSiblingRef.current.getBoundingClientRect(); + const clientNode = {top, left, bottom, right, width, height}; + + clientNode.left = props.rtl ? window.innerWidth - right : left; + clientNode.right = props.rtl ? window.innerWidth - left : right; + + calcOverflow(localContainerNode, clientNode); + adjustDirection(); + + const localArrowPosition = getArrowPosition(localContainerNode, clientNode), + localContainerPosition = getContainerPosition(localContainerNode, clientNode); - componentDidMount () { - if (this.props.open) { - on('keydown', this.handleKeyDown); - on('keyup', this.handleKeyUp); + if ((stateDirection !== adjustedDirection.current) || + (arrowPosition.left !== localArrowPosition.left) || + (arrowPosition.top !== localArrowPosition.top) || + (containerPosition.left !== localContainerPosition.left) || + (containerPosition.right !== localContainerPosition.right) || + (containerPosition.top !== localContainerPosition.top) + ) { + setArrowPosition(localArrowPosition); + setContainerPosition(localContainerPosition); + setStateDirection(adjustedDirection.current); + } + } + }; + + useEffect(() => { + // component did mount + if (open) { + on('keydown', handleKeyDown); + on('keyup', handleKeyUp); } if (typeof ResizeObserver === 'function') { - this.resizeObserver = new ResizeObserver(() => { - this.positionContextualPopup(); + resizeObserver.current = new ResizeObserver(() => { + positionContextualPopup(); }); } if (typeof MutationObserver === 'function') { - this.mutationObserver = new MutationObserver(() => { - this.positionContextualPopup(); + mutationObserver.current = new MutationObserver(() => { + positionContextualPopup(); }); } - } - - getSnapshotBeforeUpdate (prevProps, prevState) { - const snapshot = { - containerWidth: this.getContainerNodeWidth() - }; - - if (prevProps.open && !this.props.open) { - const current = Spotlight.getCurrent(); - snapshot.shouldSpotActivator = ( - // isn't set - !current || - // is on the activator, and we want to re-spot it so a11y read out can occur - current === prevState.activator || - // is within the popup - this.containerNode.contains(current) - ); - } - return snapshot; - } + // component will unmount + return () => { + if (open) { + off('keydown', handleKeyDown); + off('keyup', handleKeyUp); + } + Spotlight.remove(containerId); - componentDidUpdate (prevProps, prevState, snapshot) { - if (prevProps.direction !== this.props.direction || - snapshot.containerWidth !== this.getContainerNodeWidth() || - (prevProps.open && this.props.open)) { - this.adjustedDirection = this.props.direction; - // NOTE: `setState` is called and will cause re-render - this.positionContextualPopup(); - } + if (resizeObserver.current) { + resizeObserver.current.disconnect(); + resizeObserver.current = null; + } - if (this.props.open && !prevProps.open) { - on('keydown', this.handleKeyDown); - on('keyup', this.handleKeyUp); - } else if (!this.props.open && prevProps.open) { - off('keydown', this.handleKeyDown); - off('keyup', this.handleKeyUp); - if (snapshot && snapshot.shouldSpotActivator) { - this.spotActivator(prevState.activator); + if (mutationObserver.current) { + mutationObserver.current.disconnect(); + mutationObserver.current = null; } } - } + }, [arrowPosition, containerPosition]); - componentWillUnmount () { - if (this.props.open) { - off('keydown', this.handleKeyDown); - off('keyup', this.handleKeyUp); - } - Spotlight.remove(this.state.containerId); + const getContainerNodeWidth = () => { + return containerNode.current && containerNode.current.getBoundingClientRect().width || 0; + }; - if (this.resizeObserver) { - this.resizeObserver.disconnect(); - this.resizeObserver = null; + const spotActivator = (activator) => { + if (!Spotlight.getPointerMode() && activator && activator === Spotlight.getCurrent()) { + activator.blur(); } - - if (this.mutationObserver) { - this.mutationObserver.disconnect(); - this.mutationObserver = null; + if (!Spotlight.focus(activator)) { + Spotlight.focus(); } - } - - generateId = () => { - return Math.random().toString(36).substring(2, 10); }; - getContainerNodeWidth () { - return this.containerNode && this.containerNode.getBoundingClientRect().width || 0; - } - - updateLeaveFor (activator) { - Spotlight.set(this.state.containerId, { + // getSnapshotBeforeUpdate (prevProps, prevState) { + // const snapshot = { + // containerWidth: this.getContainerNodeWidth() + // }; + // + // if (prevProps.open && !this.props.open) { + // const current = Spotlight.getCurrent(); + // snapshot.shouldSpotActivator = ( + // // isn't set + // !current || + // // is on the activator, and we want to re-spot it so a11y read out can occur + // current === prevState.activator || + // // is within the popup + // this.containerNode.contains(current) + // ); + // } + // + // return snapshot; + // } + // + // componentDidUpdate (prevProps, prevState, snapshot) { + // if (prevProps.direction !== this.props.direction || + // snapshot.containerWidth !== this.getContainerNodeWidth() || + // (prevProps.open && this.props.open)) { + // this.adjustedDirection = this.props.direction; + // // NOTE: `setState` is called and will cause re-render + // this.positionContextualPopup(); + // } + // + // if (this.props.open && !prevProps.open) { + // on('keydown', this.handleKeyDown); + // on('keyup', this.handleKeyUp); + // } else if (!this.props.open && prevProps.open) { + // off('keydown', this.handleKeyDown); + // off('keyup', this.handleKeyUp); + // if (snapshot && snapshot.shouldSpotActivator) { + // this.spotActivator(prevState.activator); + // } + // } + // } + + useEffect(() => { + adjustedDirection.current = direction; + + // get snapshot before update + // component did update + }, [direction]); + + const updateLeaveFor = (activator) => { + Spotlight.set(containerId, { leaveFor: { up: activator, down: activator, @@ -385,11 +1063,11 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { right: activator } }); - } + }; - getContainerAdjustedPosition = () => { - const position = this.adjustedDirection; - const arr = this.adjustedDirection.split(' '); + const getContainerAdjustedPosition = () => { + const position = adjustedDirection.current; + const arr = adjustedDirection.current.split(' '); let direction = null; let anchor = null; @@ -402,39 +1080,17 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { return {anchor, direction}; }; - getContainerPosition (containerNode, clientNode) { - const position = this.centerContainerPosition(containerNode, clientNode); - const {direction} = this.getContainerAdjustedPosition(); - - switch (direction) { - case 'above': - position.top = clientNode.top - this.ARROW_OFFSET - containerNode.height - this.MARGIN; - break; - case 'below': - position.top = clientNode.bottom + this.ARROW_OFFSET + this.MARGIN; - break; - case 'right': - position.left = this.props.rtl ? clientNode.left - containerNode.width - this.ARROW_OFFSET - this.MARGIN : clientNode.right + this.ARROW_OFFSET + this.MARGIN; - break; - case 'left': - position.left = this.props.rtl ? clientNode.right + this.ARROW_OFFSET + this.MARGIN : clientNode.left - containerNode.width - this.ARROW_OFFSET - this.MARGIN; - break; - } - - return this.adjustRTL(position); - } - - centerContainerPosition (containerNode, clientNode) { + const centerContainerPosition = (containerNode, clientNode) => { const pos = {}; - const {anchor, direction} = this.getContainerAdjustedPosition(); + const {anchor, direction} = getContainerAdjustedPosition(); if (direction === 'above' || direction === 'below') { - if (this.overflow.isOverLeft) { + if (overflow.current.isOverLeft) { // anchor to the left of the screen - pos.left = this.KEEPOUT; - } else if (this.overflow.isOverRight) { + pos.left = KEEPOUT; + } else if (overflow.current.isOverRight) { // anchor to the right of the screen - pos.left = window.innerWidth - containerNode.width - this.KEEPOUT; + pos.left = window.innerWidth - containerNode.width - KEEPOUT; } else if (anchor) { if (anchor === 'center') { // center horizontally @@ -453,12 +1109,12 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { } } else if (direction === 'left' || direction === 'right') { - if (this.overflow.isOverTop) { + if (overflow.current.isOverTop) { // anchor to the top of the screen - pos.top = this.KEEPOUT; - } else if (this.overflow.isOverBottom) { + pos.top = KEEPOUT; + } else if (overflow.current.isOverBottom) { // anchor to the bottom of the screen - pos.top = window.innerHeight - containerNode.height - this.KEEPOUT; + pos.top = window.innerHeight - containerNode.height - KEEPOUT; } else if (anchor === 'middle') { // center vertically pos.top = clientNode.top - (containerNode.height - clientNode.height) / 2; @@ -472,59 +1128,91 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { } return pos; - } + }; + + const adjustRTL = (position) => { + let pos = position; + if (props.rtl) { + const tmpLeft = pos.left; + pos.left = pos.right; + pos.right = tmpLeft; + } + return pos; + }; + + const getContainerPosition = (containerNode, clientNode) => { + const position = centerContainerPosition(containerNode, clientNode); + const {direction} = getContainerAdjustedPosition(); - getArrowPosition (containerNode, clientNode) { + switch (direction) { + case 'above': + position.top = clientNode.top - ARROW_OFFSET - containerNode.height - MARGIN; + break; + case 'below': + position.top = clientNode.bottom + ARROW_OFFSET + MARGIN; + break; + case 'right': + position.left = props.rtl ? clientNode.left - containerNode.width - ARROW_OFFSET - MARGIN : clientNode.right + ARROW_OFFSET + MARGIN; + break; + case 'left': + position.left = props.rtl ? clientNode.right + ARROW_OFFSET + MARGIN : clientNode.left - containerNode.width - ARROW_OFFSET - MARGIN; + break; + } + + return adjustRTL(position); + }; + + const getArrowPosition = (containerNode, clientNode) => { const position = {}; - const {anchor, direction} = this.getContainerAdjustedPosition(); + const {anchor, direction} = getContainerAdjustedPosition(); if (direction === 'above' || direction === 'below') { - if (this.overflow.isOverRight && !this.overflow.isOverLeft) { - position.left = window.innerWidth - ((containerNode.width + this.ARROW_WIDTH) / 2) - this.KEEPOUT; - } else if (!this.overflow.isOverRight && this.overflow.isOverLeft) { - position.left = ((containerNode.width - this.ARROW_WIDTH) / 2) + this.KEEPOUT; + if (overflow.current.isOverRight && !overflow.current.isOverLeft) { + position.left = window.innerWidth - ((containerNode.width + ARROW_WIDTH) / 2) - KEEPOUT; + } else if (!overflow.current.isOverRight && overflow.current.isOverLeft) { + position.left = ((containerNode.width - ARROW_WIDTH) / 2) + KEEPOUT; } else if (anchor === 'left') { - position.left = clientNode.left + (containerNode.width - this.ARROW_WIDTH) / 2; + position.left = clientNode.left + (containerNode.width - ARROW_WIDTH) / 2; } else if (anchor === 'right') { - position.left = clientNode.right - containerNode.width + (containerNode.width - this.ARROW_WIDTH) / 2; + position.left = clientNode.right - containerNode.width + (containerNode.width - ARROW_WIDTH) / 2; } else { - position.left = clientNode.left + (clientNode.width - this.ARROW_WIDTH) / 2; + position.left = clientNode.left + (clientNode.width - ARROW_WIDTH) / 2; } - } else if (this.overflow.isOverBottom && !this.overflow.isOverTop) { - position.top = window.innerHeight - ((containerNode.height + this.ARROW_WIDTH) / 2) - this.KEEPOUT; - } else if (!this.overflow.isOverBottom && this.overflow.isOverTop) { - position.top = ((containerNode.height - this.ARROW_WIDTH) / 2) + this.KEEPOUT; + } else if (overflow.current.isOverBottom && !overflow.current.isOverTop) { + position.top = window.innerHeight - ((containerNode.height + ARROW_WIDTH) / 2) - KEEPOUT; + } else if (!overflow.current.isOverBottom && overflow.current.isOverTop) { + position.top = ((containerNode.height - ARROW_WIDTH) / 2) + KEEPOUT; } else if (anchor === 'top') { - position.top = clientNode.top + (containerNode.height - this.ARROW_WIDTH) / 2; + position.top = clientNode.top + (containerNode.height - ARROW_WIDTH) / 2; } else if (anchor === 'bottom') { - position.top = clientNode.bottom - containerNode.height + (containerNode.height - this.ARROW_WIDTH) / 2; + position.top = clientNode.bottom - containerNode.height + (containerNode.height - ARROW_WIDTH) / 2; } else { - position.top = clientNode.top + (clientNode.height - this.ARROW_WIDTH) / 2; + position.top = clientNode.top + (clientNode.height - ARROW_WIDTH) / 2; } switch (direction) { case 'above': - position.top = clientNode.top - this.ARROW_WIDTH - this.MARGIN; + position.top = clientNode.top - ARROW_WIDTH - MARGIN; break; case 'below': - position.top = clientNode.bottom + this.MARGIN; + position.top = clientNode.bottom + MARGIN; break; case 'left': - position.left = this.props.rtl ? clientNode.left + clientNode.width + this.MARGIN : clientNode.left - this.ARROW_WIDTH - this.MARGIN; + position.left = props.rtl ? clientNode.left + clientNode.width + MARGIN : clientNode.left - ARROW_WIDTH - MARGIN; break; case 'right': - position.left = this.props.rtl ? clientNode.left - this.ARROW_WIDTH - this.MARGIN : clientNode.left + clientNode.width + this.MARGIN; + position.left = props.rtl ? clientNode.left - ARROW_WIDTH - MARGIN : clientNode.left + clientNode.width + MARGIN; break; default: return {}; } - return this.adjustRTL(position); - } + return adjustRTL(position); + }; - calcOverflow (container, client) { + const calcOverflow = (container, client) => { let containerHeight, containerWidth; - const {anchor, direction} = this.getContainerAdjustedPosition(); + const {anchor, direction} = getContainerAdjustedPosition(); if (direction === 'above' || direction === 'below') { containerHeight = container.height; @@ -534,285 +1222,337 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { containerWidth = container.width; } - this.overflow = { + overflow.current = { isOverTop: anchor === 'top' && (direction === 'left' || direction === 'right') ? - !(client.top > this.KEEPOUT) : - client.top - containerHeight - this.ARROW_OFFSET - this.MARGIN - this.KEEPOUT < 0, + !(client.top > KEEPOUT) : + client.top - containerHeight - ARROW_OFFSET - MARGIN - KEEPOUT < 0, isOverBottom: anchor === 'bottom' && (direction === 'left' || direction === 'right') ? - client.bottom + this.KEEPOUT > window.innerHeight : - client.bottom + containerHeight + this.ARROW_OFFSET + this.MARGIN + this.KEEPOUT > window.innerHeight, + client.bottom + KEEPOUT > window.innerHeight : + client.bottom + containerHeight + ARROW_OFFSET + MARGIN + KEEPOUT > window.innerHeight, isOverLeft: anchor === 'left' && (direction === 'above' || direction === 'below') ? - !(client.left > this.KEEPOUT) : - client.left - containerWidth - this.ARROW_OFFSET - this.MARGIN - this.KEEPOUT < 0, + !(client.left > KEEPOUT) : + client.left - containerWidth - ARROW_OFFSET - MARGIN - KEEPOUT < 0, isOverRight: anchor === 'right' && (direction === 'above' || direction === 'below') ? - client.right + this.KEEPOUT > window.innerWidth : - client.right + containerWidth + this.ARROW_OFFSET + this.MARGIN + this.KEEPOUT > window.innerWidth + client.right + KEEPOUT > window.innerWidth : + client.right + containerWidth + ARROW_OFFSET + MARGIN + KEEPOUT > window.innerWidth }; - } - - adjustDirection () { - const {anchor, direction} = this.getContainerAdjustedPosition(); - - if (this.overflow.isOverTop && !this.overflow.isOverBottom && direction === 'above') { - this.adjustedDirection = anchor ? `below ${anchor}` : 'below'; - } else if (this.overflow.isOverBottom && !this.overflow.isOverTop && direction === 'below') { - this.adjustedDirection = anchor ? `above ${anchor}` : 'above'; - } else if (this.overflow.isOverLeft && !this.overflow.isOverRight && direction === 'left' && !this.props.rtl) { - this.adjustedDirection = anchor ? `right ${anchor}` : 'right'; - } else if (this.overflow.isOverRight && !this.overflow.isOverLeft && direction === 'right' && !this.props.rtl) { - this.adjustedDirection = anchor ? `left ${anchor}` : 'left'; - } - } - - adjustRTL (position) { - let pos = position; - if (this.props.rtl) { - const tmpLeft = pos.left; - pos.left = pos.right; - pos.right = tmpLeft; - } - return pos; - } - - /** - * Position the popup in relation to the activator. - * - * Position is based on the dimensions of the popup and its activator. If the popup does not - * fit in the specified direction, it will automatically flip to the opposite direction. - * - * @method - * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype - * @public - * @returns {undefined} - */ - positionContextualPopup = () => { - if (this.containerNode && this.clientSiblingRef?.current) { - const containerNode = this.containerNode.getBoundingClientRect(); - const {top, left, bottom, right, width, height} = this.clientSiblingRef.current.getBoundingClientRect(); - const clientNode = {top, left, bottom, right, width, height}; - - clientNode.left = this.props.rtl ? window.innerWidth - right : left; - clientNode.right = this.props.rtl ? window.innerWidth - left : right; - - this.calcOverflow(containerNode, clientNode); - this.adjustDirection(); - - const arrowPosition = this.getArrowPosition(containerNode, clientNode), - containerPosition = this.getContainerPosition(containerNode, clientNode); + }; - if ((this.state.direction !== this.adjustedDirection) || - (this.state.arrowPosition.left !== arrowPosition.left) || - (this.state.arrowPosition.top !== arrowPosition.top) || - (this.state.containerPosition.left !== containerPosition.left) || - (this.state.containerPosition.right !== containerPosition.right) || - (this.state.containerPosition.top !== containerPosition.top) - ) { - this.setState({ - direction: this.adjustedDirection, - arrowPosition, - containerPosition - }); - } + const adjustDirection = () => { + const {anchor, direction} = getContainerAdjustedPosition(); + + if (overflow.current.isOverTop && !overflow.current.isOverBottom && direction === 'above') { + adjustedDirection.current = anchor ? `below ${anchor}` : 'below'; + } else if (overflow.current.isOverBottom && !overflow.current.isOverTop && direction === 'below') { + adjustedDirection.current = anchor ? `above ${anchor}` : 'above'; + } else if (overflow.current.isOverLeft && !overflow.current.isOverRight && direction === 'left' && !props.rtl) { + adjustedDirection.current = anchor ? `right ${anchor}` : 'right'; + } else if (overflow.current.isOverRight && !overflow.current.isOverLeft && direction === 'right' && !props.rtl) { + adjustedDirection.current = anchor ? `left ${anchor}` : 'left'; } }; - getContainerNode = (node) => { - this.containerNode = node; + const getContainerNode = (node) => { + containerNode.current = node; - if (this.resizeObserver) { + if (resizeObserver.current) { if (node) { // It is not easy to trigger changed position of activator, // so we chose to observe the `div` element's size that has the real size below the root of floatLayer. // This implementation is dependent on the current structure of FloatingLayer, // so if the structure have changed, below code needs to be changed accordingly. - this.resizeObserver.observe(node?.parentElement?.parentElement); + resizeObserver.current.observe(node?.parentElement?.parentElement); } else { - this.resizeObserver.disconnect(); + resizeObserver.current.disconnect(); } } - if (this.mutationObserver) { + if (mutationObserver.current) { if (node) { - this.mutationObserver.observe(document.body, {attributes: false, childList: true, subtree: true}); + mutationObserver.current.observe(document.body, {attributes: false, childList: true, subtree: true}); } else { - this.mutationObserver.disconnect(); + mutationObserver.current.disconnect(); } } }; - handle = handle.bind(this); - - handleKeyUp = this.handle( - forProp('open', true), - forKey('enter'), - () => Spotlight.getCurrent() === this.state.activator, - stop, - forwardCustom('onClose') - ); - - handleOpen = (ev) => { - forward('onOpen', ev, this.props); - this.positionContextualPopup(); + const handleOpen = (ev) => { + forward('onOpen', ev, props); // needs a method to pass onClose inside forward + positionContextualPopup(); const current = Spotlight.getCurrent(); - this.updateLeaveFor(current); - this.setState({ - activator: current - }); - this.spotPopupContent(); + updateLeaveFor(current); + setActivator(current); + spotPopupContent(); }; - handleClose = () => { - this.updateLeaveFor(null); - this.setState({ - activator: null - }); + const handleClose = () => { + updateLeaveFor(null); + setActivator(null); }; - handleDismiss = () => { - forwardCustom('onClose')(null, this.props); + const handleDismiss = () => { + forwardCustom('onClose')(null, props); // needs a method to pass onClose inside forward }; - handleDirectionalKey (ev) { - // prevent default page scrolling - ev.preventDefault(); - // stop propagation to prevent default spotlight behavior - ev.stopPropagation(); - // set the pointer mode to false on keydown - Spotlight.setPointerMode(false); - } - - // handle key event from outside (i.e. the activator) to the popup container - handleKeyDown = (ev) => { - const {activator, containerId} = this.state; - const {spotlightRestrict} = this.props; - const current = Spotlight.getCurrent(); + // handle key event from contextual popup and closes the popup + const handleContainerKeyDown = (ev) => { + // Note: Container will be only rendered if `open`ed, therefore no need to check for `open` const direction = getDirection(ev.keyCode); if (!direction) return; - const hasSpottables = Spotlight.getSpottableDescendants(containerId).length > 0; - const spotlessSpotlightModal = spotlightRestrict === 'self-only' && !hasSpottables; - const shouldSpotPopup = current === activator && direction === PositionToDirection[this.adjustedDirection.split(' ')[0]] && hasSpottables; - - if (shouldSpotPopup || spotlessSpotlightModal) { - this.handleDirectionalKey(ev); + handleDirectionalKey(ev); - // we guard against attempting a focus change by verifying the case where a - // spotlightModal popup contains no spottable components - if (!spotlessSpotlightModal && shouldSpotPopup) { - this.spotPopupContent(); - } + // if focus moves outside the popup's container, issue the `onClose` event + if (Spotlight.move(direction) && !containerNode.current.contains(Spotlight.getCurrent())) { + forwardCustom('onClose')(null, props); // needs a method to pass onClose inside forward } }; - // handle key event from contextual popup and closes the popup - handleContainerKeyDown = (ev) => { - // Note: Container will be only rendered if `open`ed, therefore no need to check for `open` - const direction = getDirection(ev.keyCode); + // render + const { + popupComponent: PopupComponent, + popupClassName, + popupProps, + skin, + ...rest + } = props; + const idFloatLayer = `${id}_floatLayer`; + let scrimTypeLocal = scrimType; + + // 'holepunch' scrimType is specific to this component, not supported by floating layer + // so it must be swapped-out for one that FloatingLayer does support. + const holepunchScrim = (scrimTypeLocal === 'holepunch'); + if ((spotlightRestrict === 'self-only' && scrimTypeLocal === 'none') || holepunchScrim) { + scrimTypeLocal = 'transparent'; + } - if (!direction) return; + const popupPropsRef = Object.assign({}, popupProps); + const ariaProps = extractAriaProps(popupPropsRef); - this.handleDirectionalKey(ev); + if (!noSkin) { + rest.skin = skin; + } - // if focus moves outside the popup's container, issue the `onClose` event - if (Spotlight.move(direction) && !this.containerNode.contains(Spotlight.getCurrent())) { - forwardCustom('onClose')(null, this.props); - } - }; + let holeBounds; + if (clientSiblingRef?.current && holepunchScrim) { + holeBounds = clientSiblingRef.current.getBoundingClientRect(); + } - spotActivator = (activator) => { - if (!Spotlight.getPointerMode() && activator && activator === Spotlight.getCurrent()) { - activator.blur(); - } - if (!Spotlight.focus(activator)) { - Spotlight.focus(); - } - }; + delete rest.onClose; + delete rest.onOpen; + delete rest.popupSpotlightId; + delete rest.rtl; + delete rest.setApiProvider; + + if (openProp) rest[openProp] = open; + + console.log(stateDirection) + return ( +
+ +
+ {holepunchScrim ? : null} + + + +
+
+ +
+ ); + }; - spotPopupContent = () => { - const {spotlightRestrict} = this.props; - const {containerId} = this.state; - const spottableDescendants = Spotlight.getSpottableDescendants(containerId); - if (spotlightRestrict === 'self-only' && spottableDescendants.length && Spotlight.getCurrent()) { - Spotlight.getCurrent().blur(); - } + Decorator.displayName = 'ContextualPopupDecorator'; + Decorator.propTypes = /** @lends limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype */ { + /** + * The component rendered within the + * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup}. + * + * @type {Component} + * @required + * @public + */ + popupComponent: EnactPropTypes.component.isRequired, - if (!Spotlight.focus(containerId)) { - Spotlight.setActiveContainer(containerId); - } - }; + /** + * Limits the range of voice control to the popup. + * + * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype + * @type {Boolean} + * @default true + * @public + */ + 'data-webos-voice-exclusive': PropTypes.bool, - render () { - const {'data-webos-voice-exclusive': voiceExclusive, popupComponent: PopupComponent, popupClassName, noAutoDismiss, open, offset, popupProps, skin, spotlightRestrict, ...rest} = this.props; - const idFloatLayer = `${this.id}_floatLayer`; - let scrimType = rest.scrimType; - delete rest.scrimType; - - // 'holepunch' scrimType is specific to this component, not supported by floating layer - // so it must be swapped-out for one that FloatingLayer does support. - const holepunchScrim = (scrimType === 'holepunch'); - if ((spotlightRestrict === 'self-only' && scrimType === 'none') || holepunchScrim) { - scrimType = 'transparent'; - } + /** + * Direction of popup with respect to the wrapped component. + * + * @type {('above'|'above center'|'above left'|'above right'|'below'|'below center'|'below left'|'below right'|'left middle'|'left top'|'left bottom'|'right middle'|'right top'|'right bottom')} + * @default 'below center' + * @public + */ + direction: PropTypes.oneOf(['above', 'above center', 'above left', 'above right', 'below', 'below center', 'below left', 'below right', 'left middle', 'left top', 'left bottom', 'right middle', 'right top', 'right bottom']), - const popupPropsRef = Object.assign({}, popupProps); - const ariaProps = extractAriaProps(popupPropsRef); + /** + * Disables closing the popup when the user presses the cancel/back (e.g. `ESC`) key or taps outside the + * popup. + * + * @type {Boolean} + * @default false + * @public + */ + noAutoDismiss: PropTypes.bool, - if (!noSkin) { - rest.skin = skin; - } + /** + * Offset from the activator to apply to the position of the popup. + * + * Only applies when `noArrow` is `true`. + * + * @type {('none'|'overlap'|'small')} + * @default 'small' + * @public + */ + offset: PropTypes.oneOf(['none', 'overlap', 'small']), - let holeBounds; - if (this.clientSiblingRef?.current && holepunchScrim) { - holeBounds = this.clientSiblingRef.current.getBoundingClientRect(); - } + /** + * Called when the user has attempted to close the popup. + * + * This may occur either when the close button is clicked or when spotlight focus + * moves outside the boundary of the popup. Setting `spotlightRestrict` to `'self-only'` + * will prevent Spotlight focus from leaving the popup. + * + * @type {Function} + * @public + */ + onClose: PropTypes.func, - delete rest.direction; - delete rest.onClose; - delete rest.onOpen; - delete rest.popupSpotlightId; - delete rest.rtl; - delete rest.setApiProvider; - - if (openProp) rest[openProp] = open; - - return ( -
- -
- {holepunchScrim ? : null} - - - -
-
- -
- ); - } + /** + * Called when the popup is opened. + * + * @type {Function} + * @public + */ + onOpen: PropTypes.func, + + /** + * Displays the contextual popup. + * + * @type {Boolean} + * @default false + * @public + */ + open: PropTypes.bool, + + /** + * CSS class name to pass to the + * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup}. + * + * This is commonly used to set width and height of the popup. + * + * @type {String} + * @public + */ + popupClassName: PropTypes.string, + + /** + * An object containing properties to be passed to popup component. + * + * @type {Object} + * @public + */ + popupProps: PropTypes.object, + + /** + * The container ID to use with Spotlight. + * + * The spotlight container for the popup isn't created until it is open. To configure + * the container using `Spotlight.set()`, handle the `onOpen` event which is fired after + * the popup has been created and opened. + * + * @type {String} + * @public + */ + popupSpotlightId: PropTypes.string, + + /** + * Indicates the content's text direction is right-to-left. + * + * @type {Boolean} + * @private + */ + rtl: PropTypes.bool, + + /** + * Set the type of scrim to use + * + * @type {('holepunch'|'translucent'|'transparent'|'none')} + * @default 'none' + * @private + */ + scrimType: PropTypes.oneOf(['holepunch', 'translucent', 'transparent', 'none']), + + /** + * Registers the ContextualPopupDecorator component with an + * {@link core/internal/ApiDecorator.ApiDecorator|ApiDecorator}. + * + * @type {Function} + * @private + */ + setApiProvider: PropTypes.func, + + /** + * The current skin for this component. + * + * When `noSkin` is set on the config object, `skin` will only be applied to the + * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup} and not + * to the popup's activator component. + * + * @see {@link limestone/Skinnable.Skinnable.skin} + * @type {String} + * @public + */ + skin: PropTypes.string, + + /** + * Restricts or prioritizes spotlight navigation. + * + * Allowed values are: + * * `'none'` - Spotlight can move freely within and beyond the popup + * * `'self-first'` - Spotlight should prefer components within the popup over + * components beyond the popup, or + * * `'self-only'` - Spotlight can only be set within the popup + * + * @type {('none'|'self-first'|'self-only')} + * @default 'self-first' + * @public + */ + spotlightRestrict: PropTypes.oneOf(['none', 'self-first', 'self-only']) }; + + return Decorator; }); /** @@ -842,7 +1582,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { const ContextualPopupDecorator = compose( ApiDecorator({api: ['positionContextualPopup']}), I18nContextDecorator({rtlProp: 'rtl'}), - Decorator + Decorator2 ); export default ContextualPopupDecorator; From 1b9e76c86853390e953c61a7569ab789744cecd5 Mon Sep 17 00:00:00 2001 From: Ion Andrusciac Date: Wed, 12 Feb 2025 10:03:42 +0200 Subject: [PATCH 2/5] Converted ContextualPopupDecorator --- .../ContextualPopupDecorator.js | 1338 ++++------------- 1 file changed, 308 insertions(+), 1030 deletions(-) diff --git a/ContextualPopupDecorator/ContextualPopupDecorator.js b/ContextualPopupDecorator/ContextualPopupDecorator.js index e8ddea639..5068724bc 100644 --- a/ContextualPopupDecorator/ContextualPopupDecorator.js +++ b/ContextualPopupDecorator/ContextualPopupDecorator.js @@ -14,7 +14,7 @@ import {handle, forProp, forKey, forward, forwardCustom, stop} from '@enact/core import hoc from '@enact/core/hoc'; import EnactPropTypes from '@enact/core/internal/prop-types'; import {WithRef} from '@enact/core/internal/WithRef'; -import {extractAriaProps} from '@enact/core/util'; +import {extractAriaProps, setDefaultProps} from '@enact/core/util'; import {I18nContextDecorator} from '@enact/i18n/I18nDecorator'; import Spotlight, {getDirection} from '@enact/spotlight'; import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; @@ -22,7 +22,7 @@ import FloatingLayer from '@enact/ui/FloatingLayer'; import ri from '@enact/ui/resolution'; import compose from 'ramda/src/compose'; import PropTypes from 'prop-types'; -import {Component, createRef, useEffect, useRef, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; import {ContextualPopup} from './ContextualPopup'; import HolePunchScrim from './HolePunchScrim'; @@ -77,997 +77,73 @@ const defaultConfig = { openProp: 'selected' }; +const contextualPopupDecoratorDefaultProps = { + 'data-webos-voice-exclusive': true, + direction: 'below center', + noAutoDismiss: false, + offset: 'small', + open: false, + scrimType: 'none', + spotlightRestrict: 'self-first' +}; + const ContextualPopupContainer = SpotlightContainerDecorator( {enterTo: 'default-element', preserveId: true}, ContextualPopup ); -const generateId = () => { - return Math.random().toString(36).substring(2, 10); -}; - -// const Decorator = hoc(defaultConfig, (config, Wrapped) => { -// const {noArrow, noSkin, openProp} = config; -// const WrappedWithRef = WithRef(Wrapped); -// -// return class extends Component { -// static displayName = 'ContextualPopupDecorator'; -// -// static propTypes = /** @lends limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype */ { -// /** -// * The component rendered within the -// * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup}. -// * -// * @type {Component} -// * @required -// * @public -// */ -// popupComponent: EnactPropTypes.component.isRequired, -// -// /** -// * Limits the range of voice control to the popup. -// * -// * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype -// * @type {Boolean} -// * @default true -// * @public -// */ -// 'data-webos-voice-exclusive': PropTypes.bool, -// -// /** -// * Direction of popup with respect to the wrapped component. -// * -// * @type {('above'|'above center'|'above left'|'above right'|'below'|'below center'|'below left'|'below right'|'left middle'|'left top'|'left bottom'|'right middle'|'right top'|'right bottom')} -// * @default 'below center' -// * @public -// */ -// direction: PropTypes.oneOf(['above', 'above center', 'above left', 'above right', 'below', 'below center', 'below left', 'below right', 'left middle', 'left top', 'left bottom', 'right middle', 'right top', 'right bottom']), -// -// /** -// * Disables closing the popup when the user presses the cancel/back (e.g. `ESC`) key or taps outside the -// * popup. -// * -// * @type {Boolean} -// * @default false -// * @public -// */ -// noAutoDismiss: PropTypes.bool, -// -// /** -// * Offset from the activator to apply to the position of the popup. -// * -// * Only applies when `noArrow` is `true`. -// * -// * @type {('none'|'overlap'|'small')} -// * @default 'small' -// * @public -// */ -// offset: PropTypes.oneOf(['none', 'overlap', 'small']), -// -// /** -// * Called when the user has attempted to close the popup. -// * -// * This may occur either when the close button is clicked or when spotlight focus -// * moves outside the boundary of the popup. Setting `spotlightRestrict` to `'self-only'` -// * will prevent Spotlight focus from leaving the popup. -// * -// * @type {Function} -// * @public -// */ -// onClose: PropTypes.func, -// -// /** -// * Called when the popup is opened. -// * -// * @type {Function} -// * @public -// */ -// onOpen: PropTypes.func, -// -// /** -// * Displays the contextual popup. -// * -// * @type {Boolean} -// * @default false -// * @public -// */ -// open: PropTypes.bool, -// -// /** -// * CSS class name to pass to the -// * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup}. -// * -// * This is commonly used to set width and height of the popup. -// * -// * @type {String} -// * @public -// */ -// popupClassName: PropTypes.string, -// -// /** -// * An object containing properties to be passed to popup component. -// * -// * @type {Object} -// * @public -// */ -// popupProps: PropTypes.object, -// -// /** -// * The container ID to use with Spotlight. -// * -// * The spotlight container for the popup isn't created until it is open. To configure -// * the container using `Spotlight.set()`, handle the `onOpen` event which is fired after -// * the popup has been created and opened. -// * -// * @type {String} -// * @public -// */ -// popupSpotlightId: PropTypes.string, -// -// /** -// * Indicates the content's text direction is right-to-left. -// * -// * @type {Boolean} -// * @private -// */ -// rtl: PropTypes.bool, -// -// /** -// * Set the type of scrim to use -// * -// * @type {('holepunch'|'translucent'|'transparent'|'none')} -// * @default 'none' -// * @private -// */ -// scrimType: PropTypes.oneOf(['holepunch', 'translucent', 'transparent', 'none']), -// -// /** -// * Registers the ContextualPopupDecorator component with an -// * {@link core/internal/ApiDecorator.ApiDecorator|ApiDecorator}. -// * -// * @type {Function} -// * @private -// */ -// setApiProvider: PropTypes.func, -// -// /** -// * The current skin for this component. -// * -// * When `noSkin` is set on the config object, `skin` will only be applied to the -// * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup} and not -// * to the popup's activator component. -// * -// * @see {@link limestone/Skinnable.Skinnable.skin} -// * @type {String} -// * @public -// */ -// skin: PropTypes.string, -// -// /** -// * Restricts or prioritizes spotlight navigation. -// * -// * Allowed values are: -// * * `'none'` - Spotlight can move freely within and beyond the popup -// * * `'self-first'` - Spotlight should prefer components within the popup over -// * components beyond the popup, or -// * * `'self-only'` - Spotlight can only be set within the popup -// * -// * @type {('none'|'self-first'|'self-only')} -// * @default 'self-first' -// * @public -// */ -// spotlightRestrict: PropTypes.oneOf(['none', 'self-first', 'self-only']) -// }; -// -// static defaultProps = { -// 'data-webos-voice-exclusive': true, -// direction: 'below center', -// noAutoDismiss: false, -// offset: 'small', -// open: false, -// scrimType: 'none', -// spotlightRestrict: 'self-first' -// }; -// -// constructor (props) { -// super(props); -// this.state = { -// arrowPosition: {top: 0, left: 0}, -// containerPosition: {top: 0, left: 0, right: 0}, -// containerId: Spotlight.add(this.props.popupSpotlightId), -// activator: null -// }; -// -// this.resizeObserver = null; -// this.overflow = {}; -// this.adjustedDirection = this.props.direction; -// this.id = this.generateId(); -// this.clientSiblingRef = createRef(null); -// -// this.MARGIN = ri.scale(noArrow ? 0 : 12); -// this.ARROW_WIDTH = noArrow ? 0 : ri.scale(60); // svg arrow width. used for arrow positioning -// this.ARROW_OFFSET = noArrow ? 0 : ri.scale(36); // actual distance of the svg arrow displayed to offset overlaps with the container. Offset is when `noArrow` is false. -// this.KEEPOUT = ri.scale(24); // keep out distance on the edge of the screen -// -// if (props.setApiProvider) { -// props.setApiProvider(this); -// } -// } -// -// componentDidMount () { -// if (this.props.open) { -// on('keydown', this.handleKeyDown); -// on('keyup', this.handleKeyUp); -// } -// -// if (typeof ResizeObserver === 'function') { -// this.resizeObserver = new ResizeObserver(() => { -// this.positionContextualPopup(); -// }); -// } -// -// if (typeof MutationObserver === 'function') { -// this.mutationObserver = new MutationObserver(() => { -// this.positionContextualPopup(); -// }); -// } -// } -// -// getSnapshotBeforeUpdate (prevProps, prevState) { -// const snapshot = { -// containerWidth: this.getContainerNodeWidth() -// }; -// -// if (prevProps.open && !this.props.open) { -// const current = Spotlight.getCurrent(); -// snapshot.shouldSpotActivator = ( -// // isn't set -// !current || -// // is on the activator, and we want to re-spot it so a11y read out can occur -// current === prevState.activator || -// // is within the popup -// this.containerNode.contains(current) -// ); -// } -// -// return snapshot; -// } -// -// componentDidUpdate (prevProps, prevState, snapshot) { -// if (prevProps.direction !== this.props.direction || -// snapshot.containerWidth !== this.getContainerNodeWidth() || -// (prevProps.open && this.props.open)) { -// this.adjustedDirection = this.props.direction; -// // NOTE: `setState` is called and will cause re-render -// this.positionContextualPopup(); -// } -// -// if (this.props.open && !prevProps.open) { -// on('keydown', this.handleKeyDown); -// on('keyup', this.handleKeyUp); -// } else if (!this.props.open && prevProps.open) { -// off('keydown', this.handleKeyDown); -// off('keyup', this.handleKeyUp); -// if (snapshot && snapshot.shouldSpotActivator) { -// this.spotActivator(prevState.activator); -// } -// } -// } -// -// componentWillUnmount () { -// if (this.props.open) { -// off('keydown', this.handleKeyDown); -// off('keyup', this.handleKeyUp); -// } -// Spotlight.remove(this.state.containerId); -// -// if (this.resizeObserver) { -// this.resizeObserver.disconnect(); -// this.resizeObserver = null; -// } -// -// if (this.mutationObserver) { -// this.mutationObserver.disconnect(); -// this.mutationObserver = null; -// } -// } -// -// generateId = () => { -// return Math.random().toString(36).substring(2, 10); -// }; -// -// getContainerNodeWidth () { -// return this.containerNode && this.containerNode.getBoundingClientRect().width || 0; -// } -// -// updateLeaveFor (activator) { -// Spotlight.set(this.state.containerId, { -// leaveFor: { -// up: activator, -// down: activator, -// left: activator, -// right: activator -// } -// }); -// } -// -// getContainerAdjustedPosition = () => { -// const position = this.adjustedDirection; -// const arr = this.adjustedDirection.split(' '); -// let direction = null; -// let anchor = null; -// -// if (arr.length === 2) { -// [direction, anchor] = arr; -// } else { -// direction = position; -// } -// -// return {anchor, direction}; -// }; -// -// getContainerPosition (containerNode, clientNode) { -// const position = this.centerContainerPosition(containerNode, clientNode); -// const {direction} = this.getContainerAdjustedPosition(); -// -// switch (direction) { -// case 'above': -// position.top = clientNode.top - this.ARROW_OFFSET - containerNode.height - this.MARGIN; -// break; -// case 'below': -// position.top = clientNode.bottom + this.ARROW_OFFSET + this.MARGIN; -// break; -// case 'right': -// position.left = this.props.rtl ? clientNode.left - containerNode.width - this.ARROW_OFFSET - this.MARGIN : clientNode.right + this.ARROW_OFFSET + this.MARGIN; -// break; -// case 'left': -// position.left = this.props.rtl ? clientNode.right + this.ARROW_OFFSET + this.MARGIN : clientNode.left - containerNode.width - this.ARROW_OFFSET - this.MARGIN; -// break; -// } -// -// return this.adjustRTL(position); -// } -// -// centerContainerPosition (containerNode, clientNode) { -// const pos = {}; -// const {anchor, direction} = this.getContainerAdjustedPosition(); -// -// if (direction === 'above' || direction === 'below') { -// if (this.overflow.isOverLeft) { -// // anchor to the left of the screen -// pos.left = this.KEEPOUT; -// } else if (this.overflow.isOverRight) { -// // anchor to the right of the screen -// pos.left = window.innerWidth - containerNode.width - this.KEEPOUT; -// } else if (anchor) { -// if (anchor === 'center') { -// // center horizontally -// pos.left = clientNode.left + (clientNode.width - containerNode.width) / 2; -// } else if (anchor === 'left') { -// // anchor to the left side of the activator -// pos.left = clientNode.left; -// } else { -// // anchor to the right side of the activator -// pos.left = clientNode.right - containerNode.width; -// } -// } else { -// // anchor to the left side of the activator, matching its width -// pos.left = clientNode.left; -// pos.width = clientNode.width; -// } -// -// } else if (direction === 'left' || direction === 'right') { -// if (this.overflow.isOverTop) { -// // anchor to the top of the screen -// pos.top = this.KEEPOUT; -// } else if (this.overflow.isOverBottom) { -// // anchor to the bottom of the screen -// pos.top = window.innerHeight - containerNode.height - this.KEEPOUT; -// } else if (anchor === 'middle') { -// // center vertically -// pos.top = clientNode.top - (containerNode.height - clientNode.height) / 2; -// } else if (anchor === 'top') { -// // anchor to the top of the activator -// pos.top = clientNode.top; -// } else { -// // anchor to the bottom of the activator -// pos.top = clientNode.bottom - containerNode.height; -// } -// } -// -// return pos; -// } -// -// getArrowPosition (containerNode, clientNode) { -// const position = {}; -// const {anchor, direction} = this.getContainerAdjustedPosition(); -// -// if (direction === 'above' || direction === 'below') { -// if (this.overflow.isOverRight && !this.overflow.isOverLeft) { -// position.left = window.innerWidth - ((containerNode.width + this.ARROW_WIDTH) / 2) - this.KEEPOUT; -// } else if (!this.overflow.isOverRight && this.overflow.isOverLeft) { -// position.left = ((containerNode.width - this.ARROW_WIDTH) / 2) + this.KEEPOUT; -// } else if (anchor === 'left') { -// position.left = clientNode.left + (containerNode.width - this.ARROW_WIDTH) / 2; -// } else if (anchor === 'right') { -// position.left = clientNode.right - containerNode.width + (containerNode.width - this.ARROW_WIDTH) / 2; -// } else { -// position.left = clientNode.left + (clientNode.width - this.ARROW_WIDTH) / 2; -// } -// } else if (this.overflow.isOverBottom && !this.overflow.isOverTop) { -// position.top = window.innerHeight - ((containerNode.height + this.ARROW_WIDTH) / 2) - this.KEEPOUT; -// } else if (!this.overflow.isOverBottom && this.overflow.isOverTop) { -// position.top = ((containerNode.height - this.ARROW_WIDTH) / 2) + this.KEEPOUT; -// } else if (anchor === 'top') { -// position.top = clientNode.top + (containerNode.height - this.ARROW_WIDTH) / 2; -// } else if (anchor === 'bottom') { -// position.top = clientNode.bottom - containerNode.height + (containerNode.height - this.ARROW_WIDTH) / 2; -// } else { -// position.top = clientNode.top + (clientNode.height - this.ARROW_WIDTH) / 2; -// } -// -// switch (direction) { -// case 'above': -// position.top = clientNode.top - this.ARROW_WIDTH - this.MARGIN; -// break; -// case 'below': -// position.top = clientNode.bottom + this.MARGIN; -// break; -// case 'left': -// position.left = this.props.rtl ? clientNode.left + clientNode.width + this.MARGIN : clientNode.left - this.ARROW_WIDTH - this.MARGIN; -// break; -// case 'right': -// position.left = this.props.rtl ? clientNode.left - this.ARROW_WIDTH - this.MARGIN : clientNode.left + clientNode.width + this.MARGIN; -// break; -// default: -// return {}; -// } -// -// return this.adjustRTL(position); -// } -// -// calcOverflow (container, client) { -// let containerHeight, containerWidth; -// const {anchor, direction} = this.getContainerAdjustedPosition(); -// -// if (direction === 'above' || direction === 'below') { -// containerHeight = container.height; -// containerWidth = (container.width - client.width) / 2; -// } else { -// containerHeight = (container.height - client.height) / 2; -// containerWidth = container.width; -// } -// -// this.overflow = { -// isOverTop: anchor === 'top' && (direction === 'left' || direction === 'right') ? -// !(client.top > this.KEEPOUT) : -// client.top - containerHeight - this.ARROW_OFFSET - this.MARGIN - this.KEEPOUT < 0, -// isOverBottom: anchor === 'bottom' && (direction === 'left' || direction === 'right') ? -// client.bottom + this.KEEPOUT > window.innerHeight : -// client.bottom + containerHeight + this.ARROW_OFFSET + this.MARGIN + this.KEEPOUT > window.innerHeight, -// isOverLeft: anchor === 'left' && (direction === 'above' || direction === 'below') ? -// !(client.left > this.KEEPOUT) : -// client.left - containerWidth - this.ARROW_OFFSET - this.MARGIN - this.KEEPOUT < 0, -// isOverRight: anchor === 'right' && (direction === 'above' || direction === 'below') ? -// client.right + this.KEEPOUT > window.innerWidth : -// client.right + containerWidth + this.ARROW_OFFSET + this.MARGIN + this.KEEPOUT > window.innerWidth -// }; -// } -// -// adjustDirection () { -// const {anchor, direction} = this.getContainerAdjustedPosition(); -// -// if (this.overflow.isOverTop && !this.overflow.isOverBottom && direction === 'above') { -// this.adjustedDirection = anchor ? `below ${anchor}` : 'below'; -// } else if (this.overflow.isOverBottom && !this.overflow.isOverTop && direction === 'below') { -// this.adjustedDirection = anchor ? `above ${anchor}` : 'above'; -// } else if (this.overflow.isOverLeft && !this.overflow.isOverRight && direction === 'left' && !this.props.rtl) { -// this.adjustedDirection = anchor ? `right ${anchor}` : 'right'; -// } else if (this.overflow.isOverRight && !this.overflow.isOverLeft && direction === 'right' && !this.props.rtl) { -// this.adjustedDirection = anchor ? `left ${anchor}` : 'left'; -// } -// } -// -// adjustRTL (position) { -// let pos = position; -// if (this.props.rtl) { -// const tmpLeft = pos.left; -// pos.left = pos.right; -// pos.right = tmpLeft; -// } -// return pos; -// } -// -// /** -// * Position the popup in relation to the activator. -// * -// * Position is based on the dimensions of the popup and its activator. If the popup does not -// * fit in the specified direction, it will automatically flip to the opposite direction. -// * -// * @method -// * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype -// * @public -// * @returns {undefined} -// */ -// positionContextualPopup = () => { -// if (this.containerNode && this.clientSiblingRef?.current) { -// const containerNode = this.containerNode.getBoundingClientRect(); -// const {top, left, bottom, right, width, height} = this.clientSiblingRef.current.getBoundingClientRect(); -// const clientNode = {top, left, bottom, right, width, height}; -// -// clientNode.left = this.props.rtl ? window.innerWidth - right : left; -// clientNode.right = this.props.rtl ? window.innerWidth - left : right; -// -// this.calcOverflow(containerNode, clientNode); -// this.adjustDirection(); -// -// const arrowPosition = this.getArrowPosition(containerNode, clientNode), -// containerPosition = this.getContainerPosition(containerNode, clientNode); -// -// if ((this.state.direction !== this.adjustedDirection) || -// (this.state.arrowPosition.left !== arrowPosition.left) || -// (this.state.arrowPosition.top !== arrowPosition.top) || -// (this.state.containerPosition.left !== containerPosition.left) || -// (this.state.containerPosition.right !== containerPosition.right) || -// (this.state.containerPosition.top !== containerPosition.top) -// ) { -// this.setState({ -// direction: this.adjustedDirection, -// arrowPosition, -// containerPosition -// }); -// } -// } -// }; -// -// getContainerNode = (node) => { -// this.containerNode = node; -// -// if (this.resizeObserver) { -// if (node) { -// // It is not easy to trigger changed position of activator, -// // so we chose to observe the `div` element's size that has the real size below the root of floatLayer. -// // This implementation is dependent on the current structure of FloatingLayer, -// // so if the structure have changed, below code needs to be changed accordingly. -// this.resizeObserver.observe(node?.parentElement?.parentElement); -// } else { -// this.resizeObserver.disconnect(); -// } -// } -// -// if (this.mutationObserver) { -// if (node) { -// this.mutationObserver.observe(document.body, {attributes: false, childList: true, subtree: true}); -// } else { -// this.mutationObserver.disconnect(); -// } -// } -// }; -// -// handle = handle.bind(this); -// -// handleKeyUp = this.handle( -// forProp('open', true), -// forKey('enter'), -// () => Spotlight.getCurrent() === this.state.activator, -// stop, -// forwardCustom('onClose') -// ); -// -// handleOpen = (ev) => { -// forward('onOpen', ev, this.props); -// this.positionContextualPopup(); -// const current = Spotlight.getCurrent(); -// this.updateLeaveFor(current); -// this.setState({ -// activator: current -// }); -// this.spotPopupContent(); -// }; -// -// handleClose = () => { -// this.updateLeaveFor(null); -// this.setState({ -// activator: null -// }); -// }; -// -// handleDismiss = () => { -// forwardCustom('onClose')(null, this.props); -// }; -// -// handleDirectionalKey (ev) { -// // prevent default page scrolling -// ev.preventDefault(); -// // stop propagation to prevent default spotlight behavior -// ev.stopPropagation(); -// // set the pointer mode to false on keydown -// Spotlight.setPointerMode(false); -// } -// -// // handle key event from outside (i.e. the activator) to the popup container -// handleKeyDown = (ev) => { -// const {activator, containerId} = this.state; -// const {spotlightRestrict} = this.props; -// const current = Spotlight.getCurrent(); -// const direction = getDirection(ev.keyCode); -// -// if (!direction) return; -// -// const hasSpottables = Spotlight.getSpottableDescendants(containerId).length > 0; -// const spotlessSpotlightModal = spotlightRestrict === 'self-only' && !hasSpottables; -// const shouldSpotPopup = current === activator && direction === PositionToDirection[this.adjustedDirection.split(' ')[0]] && hasSpottables; -// -// if (shouldSpotPopup || spotlessSpotlightModal) { -// this.handleDirectionalKey(ev); -// -// // we guard against attempting a focus change by verifying the case where a -// // spotlightModal popup contains no spottable components -// if (!spotlessSpotlightModal && shouldSpotPopup) { -// this.spotPopupContent(); -// } -// } -// }; -// -// // handle key event from contextual popup and closes the popup -// handleContainerKeyDown = (ev) => { -// // Note: Container will be only rendered if `open`ed, therefore no need to check for `open` -// const direction = getDirection(ev.keyCode); -// -// if (!direction) return; -// -// this.handleDirectionalKey(ev); -// -// // if focus moves outside the popup's container, issue the `onClose` event -// if (Spotlight.move(direction) && !this.containerNode.contains(Spotlight.getCurrent())) { -// forwardCustom('onClose')(null, this.props); -// } -// }; -// -// spotActivator = (activator) => { -// if (!Spotlight.getPointerMode() && activator && activator === Spotlight.getCurrent()) { -// activator.blur(); -// } -// if (!Spotlight.focus(activator)) { -// Spotlight.focus(); -// } -// }; -// -// spotPopupContent = () => { -// const {spotlightRestrict} = this.props; -// const {containerId} = this.state; -// const spottableDescendants = Spotlight.getSpottableDescendants(containerId); -// if (spotlightRestrict === 'self-only' && spottableDescendants.length && Spotlight.getCurrent()) { -// Spotlight.getCurrent().blur(); -// } -// -// if (!Spotlight.focus(containerId)) { -// Spotlight.setActiveContainer(containerId); -// } -// }; -// -// render () { -// const {'data-webos-voice-exclusive': voiceExclusive, popupComponent: PopupComponent, popupClassName, noAutoDismiss, open, offset, popupProps, skin, spotlightRestrict, ...rest} = this.props; -// const idFloatLayer = `${this.id}_floatLayer`; -// let scrimType = rest.scrimType; -// delete rest.scrimType; -// -// // 'holepunch' scrimType is specific to this component, not supported by floating layer -// // so it must be swapped-out for one that FloatingLayer does support. -// const holepunchScrim = (scrimType === 'holepunch'); -// if ((spotlightRestrict === 'self-only' && scrimType === 'none') || holepunchScrim) { -// scrimType = 'transparent'; -// } -// -// const popupPropsRef = Object.assign({}, popupProps); -// const ariaProps = extractAriaProps(popupPropsRef); -// -// if (!noSkin) { -// rest.skin = skin; -// } -// -// let holeBounds; -// if (this.clientSiblingRef?.current && holepunchScrim) { -// holeBounds = this.clientSiblingRef.current.getBoundingClientRect(); -// } -// -// delete rest.direction; -// delete rest.onClose; -// delete rest.onOpen; -// delete rest.popupSpotlightId; -// delete rest.rtl; -// delete rest.setApiProvider; -// -// if (openProp) rest[openProp] = open; -// -// return ( -//
-// -//
-// {holepunchScrim ? : null} -// -// -// -//
-//
-// -//
-// ); -// } -// }; -// }); - -const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { +const Decorator = hoc(defaultConfig, (config, Wrapped) => { const {noArrow, noSkin, openProp} = config; const WrappedWithRef = WithRef(Wrapped); - const Decorator = ({ - 'data-webos-voice-exclusive': voiceExclusive = true, - direction = 'below center', - noAutoDismiss = false, - offset = 'small', - open = false, - scrimType = 'none', - spotlightRestrict = 'self-first', - ...props - }) => { - // this.state - const containerId = Spotlight.add(props.popupSpotlightId); + // eslint-disable-next-line no-shadow + const ContextualPopupDecorator = (props) => { + const componentProps = setDefaultProps(props, contextualPopupDecoratorDefaultProps); + + const [activator, setActivator] = useState(null); const [arrowPosition, setArrowPosition] = useState({top: 0, left: 0}); const [containerPosition, setContainerPosition] = useState({top: 0, left: 0, right: 0}); - const [activator, setActivator] = useState(null); - const [stateDirection, setStateDirection] = useState(direction); // new + const [direction, setDirection] = useState(''); - // refs - const resizeObserver = useRef(null); - const mutationObserver = useRef(null); // new - const containerNode = useRef(null); // new - const overflow = useRef({}); - const adjustedDirection = useRef(direction); - const id = generateId(); + const adjustedDirection = useRef(componentProps.direction); + const containerId = useRef(Spotlight.add(componentProps.popupSpotlightId)); + const containerNode = useRef(null); const clientSiblingRef = useRef(null); + const overflow = useRef({}); + const mutationObserver = useRef(null); + const prevProps = useRef(componentProps); + const snapshot = useRef(null); + const resizeObserver = useRef(null); - // allCaps variables const MARGIN = ri.scale(noArrow ? 0 : 12); - const ARROW_WIDTH = noArrow ? 0 : ri.scale(60); // svg arrow width. used for arrow positioning + const ARROW_WIDTH = noArrow ? 0 : ri.scale(60); // svg arrow width. const ARROW_OFFSET = noArrow ? 0 : ri.scale(36); // actual distance of the svg arrow displayed to offset overlaps with the container. Offset is when `noArrow` is false. const KEEPOUT = ri.scale(24); // keep out distance on the edge of the screen - // needs investigating - if (props.setApiProvider) { - props.setApiProvider(this); + if (componentProps.setApiProvider) { + componentProps.setApiProvider(); } - const handleDirectionalKey = (ev) => { - // prevent default page scrolling - ev.preventDefault(); - // stop propagation to prevent default spotlight behavior - ev.stopPropagation(); - // set the pointer mode to false on keydown - Spotlight.setPointerMode(false); - }; - - const spotPopupContent = () => { - const spottableDescendants = Spotlight.getSpottableDescendants(containerId); - if (spotlightRestrict === 'self-only' && spottableDescendants.length && Spotlight.getCurrent()) { - Spotlight.getCurrent().blur(); - } - - if (!Spotlight.focus(containerId)) { - Spotlight.setActiveContainer(containerId); - } - }; - - // handle key event from outside (i.e. the activator) to the popup container - const handleKeyDown = (ev) => { - const current = Spotlight.getCurrent(); - const direction = getDirection(ev.keyCode); - - if (!direction) return; - - const hasSpottables = Spotlight.getSpottableDescendants(containerId).length > 0; - const spotlessSpotlightModal = spotlightRestrict === 'self-only' && !hasSpottables; - const shouldSpotPopup = current === activator && direction === PositionToDirection[adjustedDirection.current.split(' ')[0]] && hasSpottables; - - if (shouldSpotPopup || spotlessSpotlightModal) { - handleDirectionalKey(ev); - - // we guard against attempting a focus change by verifying the case where a - // spotlightModal popup contains no spottable components - if (!spotlessSpotlightModal && shouldSpotPopup) { - spotPopupContent(); - } - } - }; - - // unknown how to convert this - handle = handle.bind(this); - // unknown how to convert this - const handleKeyUp = handle( - forProp('open', true), - forKey('enter'), - () => Spotlight.getCurrent() === activator, - stop, - forwardCustom('onClose') // needs a method to pass onClose inside forward - ); - - - /** - * Position the popup in relation to the activator. - * - * Position is based on the dimensions of the popup and its activator. If the popup does not - * fit in the specified direction, it will automatically flip to the opposite direction. - * - * @method - * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype - * @public - * @returns {undefined} - */ - const positionContextualPopup = () => { - if (containerNode.current && clientSiblingRef?.current) { - const localContainerNode = containerNode.current.getBoundingClientRect(); - const {top, left, bottom, right, width, height} = clientSiblingRef.current.getBoundingClientRect(); - const clientNode = {top, left, bottom, right, width, height}; - - clientNode.left = props.rtl ? window.innerWidth - right : left; - clientNode.right = props.rtl ? window.innerWidth - left : right; - - calcOverflow(localContainerNode, clientNode); - adjustDirection(); - - const localArrowPosition = getArrowPosition(localContainerNode, clientNode), - localContainerPosition = getContainerPosition(localContainerNode, clientNode); - - if ((stateDirection !== adjustedDirection.current) || - (arrowPosition.left !== localArrowPosition.left) || - (arrowPosition.top !== localArrowPosition.top) || - (containerPosition.left !== localContainerPosition.left) || - (containerPosition.right !== localContainerPosition.right) || - (containerPosition.top !== localContainerPosition.top) - ) { - setArrowPosition(localArrowPosition); - setContainerPosition(localContainerPosition); - setStateDirection(adjustedDirection.current); - } - } - }; - - useEffect(() => { - // component did mount - if (open) { - on('keydown', handleKeyDown); - on('keyup', handleKeyUp); - } - - if (typeof ResizeObserver === 'function') { - resizeObserver.current = new ResizeObserver(() => { - positionContextualPopup(); - }); - } - - if (typeof MutationObserver === 'function') { - mutationObserver.current = new MutationObserver(() => { - positionContextualPopup(); - }); - } + const generateId = useCallback(() => { + return Math.random().toString(36).substring(2, 10); + }, []); - // component will unmount - return () => { - if (open) { - off('keydown', handleKeyDown); - off('keyup', handleKeyUp); - } - Spotlight.remove(containerId); + const id = useRef(generateId()); - if (resizeObserver.current) { - resizeObserver.current.disconnect(); - resizeObserver.current = null; - } - - if (mutationObserver.current) { - mutationObserver.current.disconnect(); - mutationObserver.current = null; - } - } - }, [arrowPosition, containerPosition]); - - const getContainerNodeWidth = () => { - return containerNode.current && containerNode.current.getBoundingClientRect().width || 0; - }; - - const spotActivator = (activator) => { - if (!Spotlight.getPointerMode() && activator && activator === Spotlight.getCurrent()) { - activator.blur(); - } - if (!Spotlight.focus(activator)) { - Spotlight.focus(); + const adjustRTL = useCallback((position) => { + let pos = position; + if (componentProps.rtl) { + const tmpLeft = pos.left; + pos.left = pos.right; + pos.right = tmpLeft; } - }; - - // getSnapshotBeforeUpdate (prevProps, prevState) { - // const snapshot = { - // containerWidth: this.getContainerNodeWidth() - // }; - // - // if (prevProps.open && !this.props.open) { - // const current = Spotlight.getCurrent(); - // snapshot.shouldSpotActivator = ( - // // isn't set - // !current || - // // is on the activator, and we want to re-spot it so a11y read out can occur - // current === prevState.activator || - // // is within the popup - // this.containerNode.contains(current) - // ); - // } - // - // return snapshot; - // } - // - // componentDidUpdate (prevProps, prevState, snapshot) { - // if (prevProps.direction !== this.props.direction || - // snapshot.containerWidth !== this.getContainerNodeWidth() || - // (prevProps.open && this.props.open)) { - // this.adjustedDirection = this.props.direction; - // // NOTE: `setState` is called and will cause re-render - // this.positionContextualPopup(); - // } - // - // if (this.props.open && !prevProps.open) { - // on('keydown', this.handleKeyDown); - // on('keyup', this.handleKeyUp); - // } else if (!this.props.open && prevProps.open) { - // off('keydown', this.handleKeyDown); - // off('keyup', this.handleKeyUp); - // if (snapshot && snapshot.shouldSpotActivator) { - // this.spotActivator(prevState.activator); - // } - // } - // } - - useEffect(() => { - adjustedDirection.current = direction; - - // get snapshot before update - // component did update - }, [direction]); - - const updateLeaveFor = (activator) => { - Spotlight.set(containerId, { - leaveFor: { - up: activator, - down: activator, - left: activator, - right: activator - } - }); - }; + return pos; + }, [componentProps]); - const getContainerAdjustedPosition = () => { + const getContainerAdjustedPosition = useCallback(() => { const position = adjustedDirection.current; const arr = adjustedDirection.current.split(' '); + // eslint-disable-next-line no-shadow let direction = null; let anchor = null; @@ -1078,10 +154,12 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { } return {anchor, direction}; - }; + }, []); - const centerContainerPosition = (containerNode, clientNode) => { + // eslint-disable-next-line no-shadow + const centerContainerPosition = useCallback((containerNode, clientNode) => { const pos = {}; + // eslint-disable-next-line no-shadow const {anchor, direction} = getContainerAdjustedPosition(); if (direction === 'above' || direction === 'below') { @@ -1128,20 +206,29 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { } return pos; - }; + }, [KEEPOUT, getContainerAdjustedPosition]); - const adjustRTL = (position) => { - let pos = position; - if (props.rtl) { - const tmpLeft = pos.left; - pos.left = pos.right; - pos.right = tmpLeft; - } - return pos; - }; + const getContainerNodeWidth = useCallback(() => { + return containerNode.current && containerNode.current.getBoundingClientRect().width || 0; + }, []); - const getContainerPosition = (containerNode, clientNode) => { + // eslint-disable-next-line no-shadow + const updateLeaveFor = useCallback((activator) => { + Spotlight.set(containerId.current, { + leaveFor: { + up: activator, + down: activator, + left: activator, + right: activator + } + }); + }, []); + + // eslint-disable-next-line no-shadow + const getContainerPosition = useCallback((containerNode, clientNode) => { + // eslint-disable-next-line no-shadow const position = centerContainerPosition(containerNode, clientNode); + // eslint-disable-next-line no-shadow const {direction} = getContainerAdjustedPosition(); switch (direction) { @@ -1152,18 +239,20 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { position.top = clientNode.bottom + ARROW_OFFSET + MARGIN; break; case 'right': - position.left = props.rtl ? clientNode.left - containerNode.width - ARROW_OFFSET - MARGIN : clientNode.right + ARROW_OFFSET + MARGIN; + position.left = componentProps.rtl ? clientNode.left - containerNode.width - ARROW_OFFSET - MARGIN : clientNode.right + ARROW_OFFSET + MARGIN; break; case 'left': - position.left = props.rtl ? clientNode.right + ARROW_OFFSET + MARGIN : clientNode.left - containerNode.width - ARROW_OFFSET - MARGIN; + position.left = componentProps.rtl ? clientNode.right + ARROW_OFFSET + MARGIN : clientNode.left - containerNode.width - ARROW_OFFSET - MARGIN; break; } return adjustRTL(position); - }; + }, [ARROW_OFFSET, MARGIN, adjustRTL, centerContainerPosition, componentProps, getContainerAdjustedPosition]); - const getArrowPosition = (containerNode, clientNode) => { + // eslint-disable-next-line no-shadow + const getArrowPosition = useCallback((containerNode, clientNode) => { const position = {}; + // eslint-disable-next-line no-shadow const {anchor, direction} = getContainerAdjustedPosition(); if (direction === 'above' || direction === 'below') { @@ -1198,20 +287,21 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { position.top = clientNode.bottom + MARGIN; break; case 'left': - position.left = props.rtl ? clientNode.left + clientNode.width + MARGIN : clientNode.left - ARROW_WIDTH - MARGIN; + position.left = componentProps.rtl ? clientNode.left + clientNode.width + MARGIN : clientNode.left - ARROW_WIDTH - MARGIN; break; case 'right': - position.left = props.rtl ? clientNode.left - ARROW_WIDTH - MARGIN : clientNode.left + clientNode.width + MARGIN; + position.left = componentProps.rtl ? clientNode.left - ARROW_WIDTH - MARGIN : clientNode.left + clientNode.width + MARGIN; break; default: return {}; } return adjustRTL(position); - }; + }, [ARROW_WIDTH, KEEPOUT, MARGIN, adjustRTL, componentProps, getContainerAdjustedPosition]); - const calcOverflow = (container, client) => { + const calcOverflow = useCallback((container, client) => { let containerHeight, containerWidth; + // eslint-disable-next-line no-shadow const {anchor, direction} = getContainerAdjustedPosition(); if (direction === 'above' || direction === 'below') { @@ -1236,23 +326,64 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { client.right + KEEPOUT > window.innerWidth : client.right + containerWidth + ARROW_OFFSET + MARGIN + KEEPOUT > window.innerWidth }; - }; + }, [ARROW_OFFSET, KEEPOUT, MARGIN, getContainerAdjustedPosition]); - const adjustDirection = () => { + const adjustDirection = useCallback(() => { + // eslint-disable-next-line no-shadow const {anchor, direction} = getContainerAdjustedPosition(); if (overflow.current.isOverTop && !overflow.current.isOverBottom && direction === 'above') { adjustedDirection.current = anchor ? `below ${anchor}` : 'below'; } else if (overflow.current.isOverBottom && !overflow.current.isOverTop && direction === 'below') { adjustedDirection.current = anchor ? `above ${anchor}` : 'above'; - } else if (overflow.current.isOverLeft && !overflow.current.isOverRight && direction === 'left' && !props.rtl) { + } else if (overflow.current.isOverLeft && !overflow.current.isOverRight && direction === 'left' && !componentProps.rtl) { adjustedDirection.current = anchor ? `right ${anchor}` : 'right'; - } else if (overflow.current.isOverRight && !overflow.current.isOverLeft && direction === 'right' && !props.rtl) { + } else if (overflow.current.isOverRight && !overflow.current.isOverLeft && direction === 'right' && !componentProps.rtl) { adjustedDirection.current = anchor ? `left ${anchor}` : 'left'; } - }; + }, [componentProps, getContainerAdjustedPosition]); + + /** + * Position the popup in relation to the activator. + * + * Position is based on the dimensions of the popup and its activator. If the popup does not + * fit in the specified direction, it will automatically flip to the opposite direction. + * + * @method + * @memberof limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype + * @public + * @returns {undefined} + */ + const positionContextualPopup = useCallback(() => { + if (containerNode.current && clientSiblingRef?.current) { + const localContainerNode = containerNode.current.getBoundingClientRect(); + const {top, left, bottom, right, width, height} = clientSiblingRef.current.getBoundingClientRect(); + const clientNode = {top, left, bottom, right, width, height}; + + clientNode.left = componentProps.rtl ? window.innerWidth - right : left; + clientNode.right = componentProps.rtl ? window.innerWidth - left : right; + + calcOverflow(localContainerNode, clientNode); + adjustDirection(); - const getContainerNode = (node) => { + const localArrowPosition = getArrowPosition(localContainerNode, clientNode), + localContainerPosition = getContainerPosition(localContainerNode, clientNode); + + if ((direction !== adjustedDirection.current) || + (arrowPosition.left !== localArrowPosition.left) || + (arrowPosition.top !== localArrowPosition.top) || + (containerPosition.left !== localContainerPosition.left) || + (containerPosition.right !== localContainerPosition.right) || + (containerPosition.top !== localContainerPosition.top) + ) { + setDirection(adjustedDirection.current); + setArrowPosition(localArrowPosition); + setContainerPosition(localContainerPosition); + } + } + }, [adjustDirection, arrowPosition, calcOverflow, componentProps, containerPosition, direction, getArrowPosition, getContainerPosition]); + + const getContainerNode = useCallback((node) => { containerNode.current = node; if (resizeObserver.current) { @@ -1274,29 +405,82 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { mutationObserver.current.disconnect(); } } - }; + }, []); + + const spotPopupContent = useCallback(() => { + const spottableDescendants = Spotlight.getSpottableDescendants(containerId.current); + if (componentProps.spotlightRestrict === 'self-only' && spottableDescendants.length && Spotlight.getCurrent()) { + Spotlight.getCurrent().blur(); + } - const handleOpen = (ev) => { - forward('onOpen', ev, props); // needs a method to pass onClose inside forward + if (!Spotlight.focus(containerId.current)) { + Spotlight.setActiveContainer(containerId.current); + } + }, [componentProps]); + + const handleKeyUp = useCallback(() => { + return handle( + forProp('open', true), + forKey('enter'), + () => Spotlight.getCurrent() === activator, + stop, + forwardCustom('onClose') + ) + }, [activator]); + + const handleOpen = useCallback((ev) => { + forward('onOpen', ev, componentProps); positionContextualPopup(); const current = Spotlight.getCurrent(); updateLeaveFor(current); setActivator(current); spotPopupContent(); - }; + }, [componentProps, positionContextualPopup, spotPopupContent, updateLeaveFor]); - const handleClose = () => { + const handleClose = useCallback(() => { updateLeaveFor(null); - setActivator(null); - }; + setActivator(null) + }, [updateLeaveFor]); + + const handleDismiss = useCallback(() => { + forwardCustom('onClose')(null, componentProps); + }, [componentProps]); - const handleDismiss = () => { - forwardCustom('onClose')(null, props); // needs a method to pass onClose inside forward - }; + const handleDirectionalKey = useCallback((ev) => { + // prevent default page scrolling + ev.preventDefault(); + // stop propagation to prevent default spotlight behavior + ev.stopPropagation(); + // set the pointer mode to false on keydown + Spotlight.setPointerMode(false); + }, []); + + // handle key event from outside (i.e. the activator) to the popup container + const handleKeyDown = useCallback((ev) => { + const current = Spotlight.getCurrent(); + // eslint-disable-next-line no-shadow + const direction = getDirection(ev.keyCode); + if (!direction) return; + + const hasSpottables = Spotlight.getSpottableDescendants(containerId.current).length > 0; + const spotlessSpotlightModal = componentProps.spotlightRestrict === 'self-only' && !hasSpottables; + const shouldSpotPopup = current === activator && direction === PositionToDirection[adjustedDirection.current.split(' ')[0]] && hasSpottables; + + if (shouldSpotPopup || spotlessSpotlightModal) { + handleDirectionalKey(ev); + + // we guard against attempting a focus change by verifying the case where a + // spotlightModal popup contains no spottable components + if (!spotlessSpotlightModal && shouldSpotPopup) { + spotPopupContent(); + } + } + }, [activator, componentProps, handleDirectionalKey, spotPopupContent]); // handle key event from contextual popup and closes the popup - const handleContainerKeyDown = (ev) => { + const handleContainerKeyDown = useCallback((ev) => { // Note: Container will be only rendered if `open`ed, therefore no need to check for `open` + // eslint-disable-next-line no-shadow const direction = getDirection(ev.keyCode); if (!direction) return; @@ -1305,26 +489,119 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { // if focus moves outside the popup's container, issue the `onClose` event if (Spotlight.move(direction) && !containerNode.current.contains(Spotlight.getCurrent())) { - forwardCustom('onClose')(null, props); // needs a method to pass onClose inside forward + forwardCustom('onClose')(null, componentProps); + } + }, [componentProps, handleDirectionalKey]); + + // eslint-disable-next-line no-shadow + const spotActivator = useCallback((activator) => { + if (!Spotlight.getPointerMode() && activator && activator === Spotlight.getCurrent()) { + activator.blur(); + } + if (!Spotlight.focus(activator)) { + Spotlight.focus(); + } + }, []); + + const getSnapshotBeforeUpdate = useCallback(() => { + const localSnapshot = { + containerWidth: getContainerNodeWidth() + }; + + if (!prevProps.current.open && componentProps.open) { + const current = Spotlight.getCurrent(); + localSnapshot.shouldSpotActivator = ( + // isn't set + !current || + // is on the activator, and we want to re-spot it so a11y read out can occur + current === activator || + // is within the popup + containerNode.current?.contains(current) + ); + } + + return localSnapshot; + }, [activator, componentProps, getContainerNodeWidth]); + + useEffect(() => { + if (componentProps.open) { + on('keydown', handleKeyDown); + on('keyup', handleKeyUp); + } + + if (typeof ResizeObserver === 'function') { + resizeObserver.current = new ResizeObserver(() => { + positionContextualPopup(); + }); + } + + if (typeof MutationObserver === 'function') { + mutationObserver.current = new MutationObserver(() => { + positionContextualPopup(); + }); } - }; - - // render - const { - popupComponent: PopupComponent, - popupClassName, - popupProps, - skin, - ...rest - } = props; - const idFloatLayer = `${id}_floatLayer`; - let scrimTypeLocal = scrimType; + + snapshot.current = getSnapshotBeforeUpdate(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (prevProps.current.direction !== componentProps.direction || + snapshot.current.containerWidth !== getContainerNodeWidth() || + (prevProps.current.open && componentProps.open)) { + adjustedDirection.current = componentProps.direction; + // NOTE: `setState` is called and will cause re-render + positionContextualPopup(); + } + + if (componentProps.open && !prevProps.current.open) { + on('keydown', handleKeyDown); + on('keyup', handleKeyUp); + snapshot.current = getSnapshotBeforeUpdate(); + } else if (!componentProps.open && prevProps.current.open) { + off('keydown', handleKeyDown); + off('keyup', handleKeyUp); + if (snapshot.current && snapshot.current.shouldSpotActivator) { + spotActivator(activator); + } + } + + prevProps.current = componentProps; + }, [activator, componentProps, getContainerNodeWidth, getSnapshotBeforeUpdate, handleKeyDown, handleKeyUp, positionContextualPopup, spotActivator]); + + useEffect(() => { + // eslint-disable-next-line no-shadow + const id = containerId.current; + + return () => { + if (componentProps.open) { + off('keydown', handleKeyDown); + off('keyup', handleKeyUp); + } + Spotlight.remove(id); + + if (resizeObserver.current) { + resizeObserver.current.disconnect(); + resizeObserver.current = null; + } + + if (mutationObserver.current) { + mutationObserver.current.disconnect(); + mutationObserver.current = null; + } + }; + }, [componentProps, handleKeyDown, handleKeyUp]); + + const {'data-webos-voice-exclusive': voiceExclusive, popupComponent: PopupComponent, popupClassName, noAutoDismiss, open, offset, popupProps, skin, spotlightRestrict, ...rest} = componentProps; + const idFloatLayer = `${id.current}_floatLayer`; + let scrimType = rest.scrimType; + delete rest.scrimType; // 'holepunch' scrimType is specific to this component, not supported by floating layer // so it must be swapped-out for one that FloatingLayer does support. - const holepunchScrim = (scrimTypeLocal === 'holepunch'); - if ((spotlightRestrict === 'self-only' && scrimTypeLocal === 'none') || holepunchScrim) { - scrimTypeLocal = 'transparent'; + const holepunchScrim = (scrimType === 'holepunch'); + if ((spotlightRestrict === 'self-only' && scrimType === 'none') || holepunchScrim) { + scrimType = 'transparent'; } const popupPropsRef = Object.assign({}, popupProps); @@ -1339,6 +616,7 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { holeBounds = clientSiblingRef.current.getBoundingClientRect(); } + delete rest.direction; delete rest.onClose; delete rest.onOpen; delete rest.popupSpotlightId; @@ -1347,7 +625,6 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { if (openProp) rest[openProp] = open; - console.log(stateDirection) return (
{ onDismiss={handleDismiss} onOpen={handleOpen} open={open} - scrimType={scrimTypeLocal} + scrimType={scrimType} >
{holepunchScrim ? : null} @@ -1365,7 +642,7 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { {...ariaProps} className={popupClassName} onKeyDown={handleContainerKeyDown} - direction={stateDirection} + direction={direction} arrowPosition={arrowPosition} containerPosition={containerPosition} containerRef={getContainerNode} @@ -1373,7 +650,7 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { offset={noArrow ? offset : 'none'} showArrow={!noArrow} skin={skin} - spotlightId={containerId} + spotlightId={containerId.current} spotlightRestrict={spotlightRestrict} > @@ -1385,8 +662,9 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { ); }; - Decorator.displayName = 'ContextualPopupDecorator'; - Decorator.propTypes = /** @lends limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype */ { + ContextualPopupDecorator.displayName = 'ContextualPopupDecorator'; + + ContextualPopupDecorator.propTypes = /** @lends limestone/ContextualPopupDecorator.ContextualPopupDecorator.prototype */ { /** * The component rendered within the * {@link limestone/ContextualPopupDecorator.ContextualPopup|ContextualPopup}. @@ -1552,7 +830,7 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { spotlightRestrict: PropTypes.oneOf(['none', 'self-first', 'self-only']) }; - return Decorator; + return ContextualPopupDecorator; }); /** @@ -1582,7 +860,7 @@ const Decorator2 = hoc(defaultConfig, (config, Wrapped) => { const ContextualPopupDecorator = compose( ApiDecorator({api: ['positionContextualPopup']}), I18nContextDecorator({rtlProp: 'rtl'}), - Decorator2 + Decorator ); export default ContextualPopupDecorator; From a938c94cc9199c65c16979c5eb2a393240a45473 Mon Sep 17 00:00:00 2001 From: Ion Andrusciac Date: Wed, 12 Feb 2025 10:30:01 +0200 Subject: [PATCH 3/5] fixes --- ContextualPopupDecorator/ContextualPopupDecorator.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ContextualPopupDecorator/ContextualPopupDecorator.js b/ContextualPopupDecorator/ContextualPopupDecorator.js index 5068724bc..2a84c0b1e 100644 --- a/ContextualPopupDecorator/ContextualPopupDecorator.js +++ b/ContextualPopupDecorator/ContextualPopupDecorator.js @@ -381,7 +381,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { setContainerPosition(localContainerPosition); } } - }, [adjustDirection, arrowPosition, calcOverflow, componentProps, containerPosition, direction, getArrowPosition, getContainerPosition]); + }, [adjustDirection, arrowPosition, componentProps, containerPosition, direction, getArrowPosition, getContainerPosition]); const getContainerNode = useCallback((node) => { containerNode.current = node; @@ -516,7 +516,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { // is on the activator, and we want to re-spot it so a11y read out can occur current === activator || // is within the popup - containerNode.current?.contains(current) + containerNode.current.contains(current) ); } @@ -542,8 +542,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { } snapshot.current = getSnapshotBeforeUpdate(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [componentProps, handleKeyDown, handleKeyUp, getSnapshotBeforeUpdate]); useEffect(() => { if (prevProps.current.direction !== componentProps.direction || @@ -557,7 +556,6 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { if (componentProps.open && !prevProps.current.open) { on('keydown', handleKeyDown); on('keyup', handleKeyUp); - snapshot.current = getSnapshotBeforeUpdate(); } else if (!componentProps.open && prevProps.current.open) { off('keydown', handleKeyDown); off('keyup', handleKeyUp); From f43b374147f07ec43c557656359e0b29cd93f517 Mon Sep 17 00:00:00 2001 From: Ion Andrusciac Date: Wed, 12 Feb 2025 14:07:24 +0200 Subject: [PATCH 4/5] Component fixes --- .../ContextualPopupDecorator.js | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/ContextualPopupDecorator/ContextualPopupDecorator.js b/ContextualPopupDecorator/ContextualPopupDecorator.js index 2a84c0b1e..503f4f041 100644 --- a/ContextualPopupDecorator/ContextualPopupDecorator.js +++ b/ContextualPopupDecorator/ContextualPopupDecorator.js @@ -115,6 +115,9 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { const snapshot = useRef(null); const resizeObserver = useRef(null); + const keyDownRef = useRef(null); + const keyUpRef = useRef(null); + const MARGIN = ri.scale(noArrow ? 0 : 12); const ARROW_WIDTH = noArrow ? 0 : ri.scale(60); // svg arrow width. const ARROW_OFFSET = noArrow ? 0 : ri.scale(36); // actual distance of the svg arrow displayed to offset overlaps with the container. Offset is when `noArrow` is false. @@ -381,7 +384,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { setContainerPosition(localContainerPosition); } } - }, [adjustDirection, arrowPosition, componentProps, containerPosition, direction, getArrowPosition, getContainerPosition]); + }, [adjustDirection, arrowPosition, calcOverflow, componentProps, containerPosition, direction, getArrowPosition, getContainerPosition]); const getContainerNode = useCallback((node) => { containerNode.current = node; @@ -508,7 +511,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { containerWidth: getContainerNodeWidth() }; - if (!prevProps.current.open && componentProps.open) { + if (prevProps.current.open && !componentProps.open) { const current = Spotlight.getCurrent(); localSnapshot.shouldSpotActivator = ( // isn't set @@ -516,7 +519,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { // is on the activator, and we want to re-spot it so a11y read out can occur current === activator || // is within the popup - containerNode.current.contains(current) + containerNode.current?.contains(current) ); } @@ -525,8 +528,8 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { useEffect(() => { if (componentProps.open) { - on('keydown', handleKeyDown); - on('keyup', handleKeyUp); + on('keydown', keyDownRef.current); + on('keyup', keyUpRef.current); } if (typeof ResizeObserver === 'function') { @@ -541,10 +544,11 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { }); } - snapshot.current = getSnapshotBeforeUpdate(); - }, [componentProps, handleKeyDown, handleKeyUp, getSnapshotBeforeUpdate]); + }, [componentProps, positionContextualPopup]); useEffect(() => { + snapshot.current = getSnapshotBeforeUpdate(); + if (prevProps.current.direction !== componentProps.direction || snapshot.current.containerWidth !== getContainerNodeWidth() || (prevProps.current.open && componentProps.open)) { @@ -554,29 +558,26 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { } if (componentProps.open && !prevProps.current.open) { - on('keydown', handleKeyDown); - on('keyup', handleKeyUp); + on('keydown', keyDownRef.current); + on('keyup', keyUpRef.current); } else if (!componentProps.open && prevProps.current.open) { - off('keydown', handleKeyDown); - off('keyup', handleKeyUp); + off('keydown', keyDownRef.current); + off('keyup', keyUpRef.current); if (snapshot.current && snapshot.current.shouldSpotActivator) { spotActivator(activator); } } prevProps.current = componentProps; - }, [activator, componentProps, getContainerNodeWidth, getSnapshotBeforeUpdate, handleKeyDown, handleKeyUp, positionContextualPopup, spotActivator]); + }, [activator, componentProps, getContainerNodeWidth, getSnapshotBeforeUpdate, positionContextualPopup, spotActivator]); useEffect(() => { - // eslint-disable-next-line no-shadow - const id = containerId.current; - return () => { if (componentProps.open) { - off('keydown', handleKeyDown); - off('keyup', handleKeyUp); + off('keydown', keyDownRef.current); + off('keyup', keyUpRef.current); } - Spotlight.remove(id); + Spotlight.remove(containerId.current); if (resizeObserver.current) { resizeObserver.current.disconnect(); @@ -588,6 +589,21 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { mutationObserver.current = null; } }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (componentProps.open) { + if (handleKeyDown !== keyDownRef.current || handleKeyUp !== keyUpRef.current) { + off('keydown', keyDownRef.current); + off('keyup', keyUpRef.current); + keyDownRef.current = handleKeyDown; + keyUpRef.current = handleKeyUp; + on('keydown', keyDownRef.current); + on('keyup', keyUpRef.current); + } + } }, [componentProps, handleKeyDown, handleKeyUp]); const {'data-webos-voice-exclusive': voiceExclusive, popupComponent: PopupComponent, popupClassName, noAutoDismiss, open, offset, popupProps, skin, spotlightRestrict, ...rest} = componentProps; From 794842c1dbfd08672ec4f06a8c52eed516f7c90e Mon Sep 17 00:00:00 2001 From: Ion Andrusciac Date: Wed, 12 Feb 2025 14:17:27 +0200 Subject: [PATCH 5/5] Fixed lint errors --- ContextualPopupDecorator/ContextualPopupDecorator.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ContextualPopupDecorator/ContextualPopupDecorator.js b/ContextualPopupDecorator/ContextualPopupDecorator.js index 503f4f041..70b341e7b 100644 --- a/ContextualPopupDecorator/ContextualPopupDecorator.js +++ b/ContextualPopupDecorator/ContextualPopupDecorator.js @@ -428,7 +428,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { () => Spotlight.getCurrent() === activator, stop, forwardCustom('onClose') - ) + ); }, [activator]); const handleOpen = useCallback((ev) => { @@ -442,7 +442,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { const handleClose = useCallback(() => { updateLeaveFor(null); - setActivator(null) + setActivator(null); }, [updateLeaveFor]); const handleDismiss = useCallback(() => { @@ -572,12 +572,15 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => { }, [activator, componentProps, getContainerNodeWidth, getSnapshotBeforeUpdate, positionContextualPopup, spotActivator]); useEffect(() => { + // eslint-disable-next-line no-shadow + const id = containerId.current; + return () => { if (componentProps.open) { off('keydown', keyDownRef.current); off('keyup', keyUpRef.current); } - Spotlight.remove(containerId.current); + Spotlight.remove(id); if (resizeObserver.current) { resizeObserver.current.disconnect();