@orfeas126/box-ui-elements
Version:
Box UI Elements
238 lines (227 loc) • 8.02 kB
JavaScript
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