UNPKG

@orfeas126/box-ui-elements

Version:
238 lines (227 loc) 8.02 kB
import PropTypes from 'prop-types'; import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import Tooltip from '../../components/tooltip'; import DragCloud from './DragCloud'; import DropCloud from './DropCloud'; import messages from './messages'; import { checkOverlap, getGridPosition, getRandomCloudPosition } from './utils'; import './SecurityCloudGame.scss'; // pick these numbers to balance accessibility and game complexity const CLOUD_SIZE_RATIO = 4; const GRID_TRACK_SIZE_RATIO = 16; const SecurityCloudGame = ({ height, intl: { formatMessage }, onValidDrop, width }) => { const [dropCloudPosition, setDropCloudPosition] = useState(null); const [dragCloudPosition, setDragCloudPosition] = useState(null); const [layout, setLayout] = useState({}); // game interaction states const [liveText, setLiveText] = useState(''); const [isOverlap, setIsOverlap] = useState(false); const [isValidDrop, setIsValidDrop] = useState(false); const messageElementRef = useRef(); // to handle resize events const gameBoardSizeRef = useRef({}); const { cloudSize, gameBoardHeight, gridTrackSize } = layout; useLayoutEffect(() => { const { current: messageElement } = messageElementRef; const newGameBoardHeight = height - messageElement.getBoundingClientRect().height; // guardrail to prevent further rendering if the game board height is not positive if (newGameBoardHeight <= 0) return; const minGameBoardLength = Math.min(newGameBoardHeight, width); setLayout({ gameBoardHeight: newGameBoardHeight, cloudSize: minGameBoardLength / CLOUD_SIZE_RATIO, gridTrackSize: minGameBoardLength / GRID_TRACK_SIZE_RATIO }); }, [height, width]); useEffect(() => { if (!gameBoardHeight) { return; } const { height: prevHeight, width: prevWidth } = gameBoardSizeRef.current; const heightRatio = prevHeight ? gameBoardHeight / prevHeight : 1; const widthRatio = prevWidth ? width / prevWidth : 1; // declare and update this variable first in order to generate the starting position for drag cloud let newDropCloudPosition; // use prevState => {} to avoid referencing and updating the state at the same time setDropCloudPosition(prevPos => { newDropCloudPosition = prevPos ? { x: prevPos.x * widthRatio, y: prevPos.y * heightRatio } // on board resize : getRandomCloudPosition(cloudSize, gameBoardHeight, width); // initial render return newDropCloudPosition; }); setDragCloudPosition(prevPos => { // on board resize if (prevPos) { return { x: prevPos.x * widthRatio, y: prevPos.y * heightRatio }; } let nextPos = getRandomCloudPosition(cloudSize, gameBoardHeight, width); // keep generating new random position until there is no overlap while (checkOverlap(nextPos, newDropCloudPosition, cloudSize)) { nextPos = getRandomCloudPosition(cloudSize, gameBoardHeight, width); } return nextPos; }); // update previous height and width for ratio calculation gameBoardSizeRef.current = { height: gameBoardHeight, width }; }, [cloudSize, gameBoardHeight, width]); /** * Update real-time instructional messages for screen reader users. * @param {string}} text - assistive text for screen readers * @param {boolean} includeTargetPosition - if target/drop cloud position should be included */ const updateLiveText = (text, includeTargetPosition = false) => { if (includeTargetPosition) { const targetPositionText = formatMessage(messages.targetPosition, getGridPosition(dropCloudPosition, gridTrackSize)); text += ` ${targetPositionText}`; } setLiveText(text); }; /** * DragCloud drop event handler. Checks if it's valid drop and handles valid drop if it is. * @returns {void} */ const onDrop = () => { if (isOverlap) { setIsValidDrop(true); updateLiveText(formatMessage(messages.success)); if (onValidDrop) { // call onValidDrop if passed in through props onValidDrop(); } } }; /** * Pass along to the drag cloud to set position on moving. * @param {number} newPosition - new drag cloud position * @param {boolean} shouldUpdateLiveText - default to false * @returns {void} */ const updatePosition = (newPosition, shouldUpdateLiveText = false) => { setDragCloudPosition(newPosition); const hasOverlap = checkOverlap(newPosition, dropCloudPosition, cloudSize); setIsOverlap(hasOverlap); if (shouldUpdateLiveText) { const newliveText = hasOverlap ? formatMessage(messages.targetInRange) : formatMessage(messages.currentPosition, getGridPosition(newPosition, gridTrackSize)); updateLiveText(newliveText, !hasOverlap); } }; /** * Get aria label for the message element. * @returns {string|undefined} */ const getAccessibilityInstructions = () => gameBoardHeight && cloudSize && gridTrackSize && formatMessage(messages.accessibilityInstructions, getGridPosition({ x: width - cloudSize, y: gameBoardHeight - cloudSize }, gridTrackSize)); /** * Renders the drop cloud. * @returns {JSX} */ const renderDropCloud = () => { if (dropCloudPosition && !isValidDrop) { return /*#__PURE__*/React.createElement(DropCloud, { className: isOverlap ? 'is-over' : '', cloudSize: cloudSize, position: dropCloudPosition }); } return null; }; /** * Renders the drag cloud. * @returns {JSX} */ const renderDragCloud = () => { const { current: gameBoardSize } = gameBoardSizeRef; if (dragCloudPosition) { return /*#__PURE__*/React.createElement(DragCloud, { cloudSize: cloudSize, disabled: isValidDrop, gameBoardSize: gameBoardSize, gridTrackSize: gridTrackSize, onDrop: onDrop, position: dragCloudPosition, updateLiveText: updateLiveText, updatePosition: updatePosition }); } return null; }; /** * Renders the message shown to the user * @returns {JSX} */ const renderMessage = () => { if (isValidDrop) { return /*#__PURE__*/React.createElement(FormattedMessage, messages.success); } return /*#__PURE__*/React.createElement(FormattedMessage, messages.instructions); }; /** * Renders the cloud game * @returns {JSX} */ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { className: "bdl-SecurityCloudGame-liveText", "aria-live": "polite" }, liveText), /*#__PURE__*/React.createElement("div", { className: "bdl-SecurityCloudGame", style: { height: `${height}px`, width: `${width}px` } }, /*#__PURE__*/React.createElement(Tooltip, { ariaHidden: true, className: "bdl-SecurityCloudGame-tooltip", constrainToWindow: false, position: "bottom-center", text: renderMessage() }, /*#__PURE__*/React.createElement("div", { ref: messageElementRef, className: "bdl-SecurityCloudGame-message", "aria-label": getAccessibilityInstructions() }, renderMessage())), /*#__PURE__*/React.createElement("div", { className: "bdl-SecurityCloudGame-board" }, renderDropCloud(), renderDragCloud()))); }; SecurityCloudGame.displayName = 'SecurityCloudGame'; SecurityCloudGame.propTypes = { /** Height to set the game to */ height: PropTypes.number.isRequired, /* Intl object */ intl: PropTypes.any, /** Function to call when the `DragCloud` is successfully dropped onto the `DropCloud` */ onValidDrop: PropTypes.func, /** Width to set the game to */ width: PropTypes.number.isRequired }; export { SecurityCloudGame as SecurityCloudGameBase }; export default injectIntl(SecurityCloudGame); //# sourceMappingURL=SecurityCloudGame.js.map