@reach/popover
Advanced tools
Comparing version 0.0.1 to 0.0.2
182
es/index.js
@@ -5,6 +5,7 @@ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
import React, { useRef, forwardRef } from "react"; | ||
import React, { useRef, forwardRef, useEffect } from "react"; | ||
import Portal from "@reach/portal"; | ||
import { useRect } from "@reach/rect"; | ||
import { assignRef } from "@reach/utils"; | ||
import tabbable from "tabbable"; | ||
@@ -28,9 +29,12 @@ export default forwardRef(function Popover(props, ref) { | ||
var popupRef = useRef(); | ||
var popupRect = useRect(popupRef); | ||
var popoverRef = useRef(); | ||
var popoverRect = useRect(popoverRef); | ||
var targetRect = useRect(targetRef); | ||
useSimulateTabNavigationForReactTree(targetRef, popoverRef); | ||
return React.createElement("div", _extends({ | ||
"data-reach-popover": "", | ||
ref: function ref(node) { | ||
assignRef(popupRef, node); | ||
assignRef(popoverRef, node); | ||
assignRef(forwardedRef, node); | ||
@@ -40,16 +44,16 @@ }, | ||
position: "absolute" | ||
}, getStyles(position, targetRect, popupRect)) | ||
}, getStyles(position, targetRect, popoverRect)) | ||
}, rest)); | ||
}); | ||
var getStyles = function getStyles(position, targetRect, popupRect) { | ||
var needToMeasurePopup = !popupRect; | ||
var getStyles = function getStyles(position, targetRect, popoverRect) { | ||
var needToMeasurePopup = !popoverRect; | ||
if (needToMeasurePopup) { | ||
return { visibility: "hidden" }; | ||
} | ||
return position(targetRect, popupRect); | ||
return position(targetRect, popoverRect); | ||
}; | ||
export function positionDefault(targetRect, popupRect) { | ||
var _getCollisions = getCollisions(targetRect, popupRect), | ||
export function positionDefault(targetRect, popoverRect) { | ||
var _getCollisions = getCollisions(targetRect, popoverRect), | ||
directionUp = _getCollisions.directionUp, | ||
@@ -59,9 +63,9 @@ directionRight = _getCollisions.directionRight; | ||
return { | ||
left: directionRight ? targetRect.right - popupRect.width + window.pageXOffset + "px" : targetRect.left + window.pageXOffset + "px", | ||
top: directionUp ? targetRect.top - popupRect.height + window.pageYOffset + "px" : targetRect.top + targetRect.height + window.pageYOffset + "px" | ||
left: directionRight ? targetRect.right - popoverRect.width + window.pageXOffset + "px" : targetRect.left + window.pageXOffset + "px", | ||
top: directionUp ? targetRect.top - popoverRect.height + window.pageYOffset + "px" : targetRect.top + targetRect.height + window.pageYOffset + "px" | ||
}; | ||
} | ||
export function positionMatchWidth(targetRect, popupRect) { | ||
var _getCollisions2 = getCollisions(targetRect, popupRect), | ||
export function positionMatchWidth(targetRect, popoverRect) { | ||
var _getCollisions2 = getCollisions(targetRect, popoverRect), | ||
directionUp = _getCollisions2.directionUp; | ||
@@ -72,3 +76,3 @@ | ||
left: targetRect.left, | ||
top: directionUp ? targetRect.top - popupRect.height + window.pageYOffset + "px" : targetRect.top + targetRect.height + window.pageYOffset + "px" | ||
top: directionUp ? targetRect.top - popoverRect.height + window.pageYOffset + "px" : targetRect.top + targetRect.height + window.pageYOffset + "px" | ||
}; | ||
@@ -78,9 +82,9 @@ } | ||
// Finish this another time | ||
// export function positionHorizontalCenter(targetRect, popupRect) { | ||
// export function positionHorizontalCenter(targetRect, popoverRect) { | ||
// const targetCenter = targetRect.width / 2 + targetRect.left; | ||
// const popupHalf = popupRect.width / 2; | ||
// const popoverHalf = popoverRect.width / 2; | ||
// const collisions = { | ||
// right: window.innerWidth < targetCenter - popupHalf, | ||
// left: targetCenter - popupHalf < 0 | ||
// right: window.innerWidth < targetCenter - popoverHalf, | ||
// left: targetCenter - popoverHalf < 0 | ||
// // top: | ||
@@ -92,3 +96,3 @@ // // bottom: | ||
// left: collisions.right | ||
// ? `${targetRect.right - popupRect.width + window.pageXOffset}px` | ||
// ? `${targetRect.right - popoverRect.width + window.pageXOffset}px` | ||
// : collisions.left ? `` : `` | ||
@@ -98,3 +102,3 @@ // }; | ||
function getCollisions(targetRect, popupRect) { | ||
function getCollisions(targetRect, popoverRect) { | ||
var offsetLeft = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; | ||
@@ -104,6 +108,6 @@ var offsetBottom = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; | ||
var collisions = { | ||
top: targetRect.top - popupRect.height < 0, | ||
right: window.innerWidth < targetRect.left + popupRect.width - offsetLeft, | ||
bottom: window.innerHeight < targetRect.bottom + popupRect.height - offsetBottom, | ||
left: targetRect.left - popupRect.width < 0 | ||
top: targetRect.top - popoverRect.height < 0, | ||
right: window.innerWidth < targetRect.left + popoverRect.width - offsetLeft, | ||
bottom: window.innerHeight < targetRect.bottom + popoverRect.height - offsetBottom, | ||
left: targetRect.left - popoverRect.width < 0 | ||
}; | ||
@@ -115,2 +119,130 @@ | ||
return { directionRight: directionRight, directionUp: directionUp }; | ||
} | ||
// Heads up, my jQuery past haunts this function. This hook scopes the tab | ||
// order to the React element tree, instead of the DOM tree. This way, when the | ||
// user navigates with tab from the targetRef, the tab order moves into the | ||
// popup, and then out of the popup back to the rest of the document. | ||
// (We call targetRef, triggerRef inside this function to avoid confusion with | ||
// event.target) | ||
function useSimulateTabNavigationForReactTree(triggerRef, popoverRef) { | ||
var doc = triggerRef.current.ownerDocument; // maybe in devtools | ||
function handleKeyDown(event) { | ||
if (event.key === "Tab" && tabbable(popoverRef.current).length === 0) { | ||
return; | ||
} | ||
if (event.key === "Tab" && event.shiftKey) { | ||
if (shiftTabbedFromElementAfterTrigger(event)) { | ||
focusLastTabbableInPopover(event); | ||
} else if (shiftTabbedOutOfPopover(event)) { | ||
focusTriggerRef(event); | ||
} else if (shiftTabbedToBrowserChrome(event)) { | ||
disableTabbablesInPopover(event); | ||
} | ||
} else if (event.key === "Tab") { | ||
if (tabbedFromTriggerToPopover(event)) { | ||
focusFirstPopoverTabbable(event); | ||
} else if (tabbedOutOfPopover(event)) { | ||
focusTabbableAfterTrigger(event); | ||
} else if (tabbedToBrowserChrome(event)) { | ||
disableTabbablesInPopover(event); | ||
} | ||
} | ||
} | ||
useEffect(function () { | ||
doc.addEventListener("keydown", handleKeyDown); | ||
return function () { | ||
return doc.removeEventListener("keydown", handleKeyDown); | ||
}; | ||
}, []); | ||
function getElementAfterTrigger() { | ||
var elements = tabbable(doc); | ||
var targetIndex = elements.indexOf(triggerRef.current); | ||
return elements[targetIndex + 1]; | ||
} | ||
function tabbedFromTriggerToPopover() { | ||
return triggerRef.current === document.activeElement; | ||
} | ||
function focusFirstPopoverTabbable(event) { | ||
var elements = tabbable(popoverRef.current); | ||
if (elements[0]) { | ||
event.preventDefault(); | ||
elements[0].focus(); | ||
} | ||
} | ||
function tabbedOutOfPopover(event) { | ||
var inPopover = popoverRef.current.contains(document.activeElement); | ||
if (inPopover) { | ||
var elements = tabbable(popoverRef.current); | ||
return elements[elements.length - 1] === document.activeElement; | ||
} | ||
} | ||
function focusTabbableAfterTrigger(event) { | ||
var elementAfterTrigger = getElementAfterTrigger(); | ||
if (elementAfterTrigger) { | ||
event.preventDefault(); | ||
elementAfterTrigger.focus(); | ||
} | ||
} | ||
function shiftTabbedFromElementAfterTrigger(event) { | ||
if (!event.shiftKey) return; | ||
var elementAfterTrigger = getElementAfterTrigger(); | ||
return event.target === elementAfterTrigger; | ||
} | ||
function focusLastTabbableInPopover(event) { | ||
var elements = tabbable(popoverRef.current); | ||
var last = elements[elements.length - 1]; | ||
if (last) { | ||
event.preventDefault(); | ||
last.focus(); | ||
} | ||
} | ||
function shiftTabbedOutOfPopover(event) { | ||
var elements = tabbable(popoverRef.current); | ||
return elements.length === 0 ? false : event.target === elements[0]; | ||
} | ||
function focusTriggerRef(event) { | ||
event.preventDefault(); | ||
triggerRef.current.focus(); | ||
} | ||
function tabbedToBrowserChrome(event) { | ||
var elements = tabbable(doc).filter(function (element) { | ||
return !popoverRef.current.contains(element); | ||
}); | ||
return event.target === elements[elements.length - 1]; | ||
} | ||
function shiftTabbedToBrowserChrome(event) { | ||
// we're assuming the popover will never contain the first tabbable | ||
// element, and it better not, because the trigger needs to be tabbable! | ||
return event.target === tabbable(doc)[0]; | ||
} | ||
var restoreTabIndexTuplés = []; | ||
function disableTabbablesInPopover() { | ||
var elements = tabbable(popoverRef.current); | ||
elements.forEach(function (element) { | ||
restoreTabIndexTuplés.push([element, element.tabIndex]); | ||
element.tabIndex = -1; | ||
}); | ||
doc.addEventListener("focusin", enableTabbablesInPopover); | ||
} | ||
function enableTabbablesInPopover(event) { | ||
doc.removeEventListener("focusin", enableTabbablesInPopover); | ||
restoreTabIndexTuplés.forEach(function (_ref2) { | ||
var element = _ref2[0], | ||
tabIndex = _ref2[1]; | ||
element.tabIndex = tabIndex; | ||
}); | ||
} | ||
} |
183
index.js
@@ -22,2 +22,6 @@ "use strict"; | ||
var _tabbable = require("tabbable"); | ||
var _tabbable2 = _interopRequireDefault(_tabbable); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -45,9 +49,12 @@ | ||
var popupRef = (0, _react.useRef)(); | ||
var popupRect = (0, _rect.useRect)(popupRef); | ||
var popoverRef = (0, _react.useRef)(); | ||
var popoverRect = (0, _rect.useRect)(popoverRef); | ||
var targetRect = (0, _rect.useRect)(targetRef); | ||
useSimulateTabNavigationForReactTree(targetRef, popoverRef); | ||
return _react2.default.createElement("div", _extends({ | ||
"data-reach-popover": "", | ||
ref: function ref(node) { | ||
(0, _utils.assignRef)(popupRef, node); | ||
(0, _utils.assignRef)(popoverRef, node); | ||
(0, _utils.assignRef)(forwardedRef, node); | ||
@@ -57,16 +64,16 @@ }, | ||
position: "absolute" | ||
}, getStyles(position, targetRect, popupRect)) | ||
}, getStyles(position, targetRect, popoverRect)) | ||
}, rest)); | ||
}); | ||
var getStyles = function getStyles(position, targetRect, popupRect) { | ||
var needToMeasurePopup = !popupRect; | ||
var getStyles = function getStyles(position, targetRect, popoverRect) { | ||
var needToMeasurePopup = !popoverRect; | ||
if (needToMeasurePopup) { | ||
return { visibility: "hidden" }; | ||
} | ||
return position(targetRect, popupRect); | ||
return position(targetRect, popoverRect); | ||
}; | ||
function positionDefault(targetRect, popupRect) { | ||
var _getCollisions = getCollisions(targetRect, popupRect), | ||
function positionDefault(targetRect, popoverRect) { | ||
var _getCollisions = getCollisions(targetRect, popoverRect), | ||
directionUp = _getCollisions.directionUp, | ||
@@ -76,9 +83,9 @@ directionRight = _getCollisions.directionRight; | ||
return { | ||
left: directionRight ? targetRect.right - popupRect.width + window.pageXOffset + "px" : targetRect.left + window.pageXOffset + "px", | ||
top: directionUp ? targetRect.top - popupRect.height + window.pageYOffset + "px" : targetRect.top + targetRect.height + window.pageYOffset + "px" | ||
left: directionRight ? targetRect.right - popoverRect.width + window.pageXOffset + "px" : targetRect.left + window.pageXOffset + "px", | ||
top: directionUp ? targetRect.top - popoverRect.height + window.pageYOffset + "px" : targetRect.top + targetRect.height + window.pageYOffset + "px" | ||
}; | ||
} | ||
function positionMatchWidth(targetRect, popupRect) { | ||
var _getCollisions2 = getCollisions(targetRect, popupRect), | ||
function positionMatchWidth(targetRect, popoverRect) { | ||
var _getCollisions2 = getCollisions(targetRect, popoverRect), | ||
directionUp = _getCollisions2.directionUp; | ||
@@ -89,3 +96,3 @@ | ||
left: targetRect.left, | ||
top: directionUp ? targetRect.top - popupRect.height + window.pageYOffset + "px" : targetRect.top + targetRect.height + window.pageYOffset + "px" | ||
top: directionUp ? targetRect.top - popoverRect.height + window.pageYOffset + "px" : targetRect.top + targetRect.height + window.pageYOffset + "px" | ||
}; | ||
@@ -95,9 +102,9 @@ } | ||
// Finish this another time | ||
// export function positionHorizontalCenter(targetRect, popupRect) { | ||
// export function positionHorizontalCenter(targetRect, popoverRect) { | ||
// const targetCenter = targetRect.width / 2 + targetRect.left; | ||
// const popupHalf = popupRect.width / 2; | ||
// const popoverHalf = popoverRect.width / 2; | ||
// const collisions = { | ||
// right: window.innerWidth < targetCenter - popupHalf, | ||
// left: targetCenter - popupHalf < 0 | ||
// right: window.innerWidth < targetCenter - popoverHalf, | ||
// left: targetCenter - popoverHalf < 0 | ||
// // top: | ||
@@ -109,3 +116,3 @@ // // bottom: | ||
// left: collisions.right | ||
// ? `${targetRect.right - popupRect.width + window.pageXOffset}px` | ||
// ? `${targetRect.right - popoverRect.width + window.pageXOffset}px` | ||
// : collisions.left ? `` : `` | ||
@@ -115,3 +122,3 @@ // }; | ||
function getCollisions(targetRect, popupRect) { | ||
function getCollisions(targetRect, popoverRect) { | ||
var offsetLeft = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; | ||
@@ -121,6 +128,6 @@ var offsetBottom = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; | ||
var collisions = { | ||
top: targetRect.top - popupRect.height < 0, | ||
right: window.innerWidth < targetRect.left + popupRect.width - offsetLeft, | ||
bottom: window.innerHeight < targetRect.bottom + popupRect.height - offsetBottom, | ||
left: targetRect.left - popupRect.width < 0 | ||
top: targetRect.top - popoverRect.height < 0, | ||
right: window.innerWidth < targetRect.left + popoverRect.width - offsetLeft, | ||
bottom: window.innerHeight < targetRect.bottom + popoverRect.height - offsetBottom, | ||
left: targetRect.left - popoverRect.width < 0 | ||
}; | ||
@@ -132,2 +139,130 @@ | ||
return { directionRight: directionRight, directionUp: directionUp }; | ||
} | ||
// Heads up, my jQuery past haunts this function. This hook scopes the tab | ||
// order to the React element tree, instead of the DOM tree. This way, when the | ||
// user navigates with tab from the targetRef, the tab order moves into the | ||
// popup, and then out of the popup back to the rest of the document. | ||
// (We call targetRef, triggerRef inside this function to avoid confusion with | ||
// event.target) | ||
function useSimulateTabNavigationForReactTree(triggerRef, popoverRef) { | ||
var doc = triggerRef.current.ownerDocument; // maybe in devtools | ||
function handleKeyDown(event) { | ||
if (event.key === "Tab" && (0, _tabbable2.default)(popoverRef.current).length === 0) { | ||
return; | ||
} | ||
if (event.key === "Tab" && event.shiftKey) { | ||
if (shiftTabbedFromElementAfterTrigger(event)) { | ||
focusLastTabbableInPopover(event); | ||
} else if (shiftTabbedOutOfPopover(event)) { | ||
focusTriggerRef(event); | ||
} else if (shiftTabbedToBrowserChrome(event)) { | ||
disableTabbablesInPopover(event); | ||
} | ||
} else if (event.key === "Tab") { | ||
if (tabbedFromTriggerToPopover(event)) { | ||
focusFirstPopoverTabbable(event); | ||
} else if (tabbedOutOfPopover(event)) { | ||
focusTabbableAfterTrigger(event); | ||
} else if (tabbedToBrowserChrome(event)) { | ||
disableTabbablesInPopover(event); | ||
} | ||
} | ||
} | ||
(0, _react.useEffect)(function () { | ||
doc.addEventListener("keydown", handleKeyDown); | ||
return function () { | ||
return doc.removeEventListener("keydown", handleKeyDown); | ||
}; | ||
}, []); | ||
function getElementAfterTrigger() { | ||
var elements = (0, _tabbable2.default)(doc); | ||
var targetIndex = elements.indexOf(triggerRef.current); | ||
return elements[targetIndex + 1]; | ||
} | ||
function tabbedFromTriggerToPopover() { | ||
return triggerRef.current === document.activeElement; | ||
} | ||
function focusFirstPopoverTabbable(event) { | ||
var elements = (0, _tabbable2.default)(popoverRef.current); | ||
if (elements[0]) { | ||
event.preventDefault(); | ||
elements[0].focus(); | ||
} | ||
} | ||
function tabbedOutOfPopover(event) { | ||
var inPopover = popoverRef.current.contains(document.activeElement); | ||
if (inPopover) { | ||
var elements = (0, _tabbable2.default)(popoverRef.current); | ||
return elements[elements.length - 1] === document.activeElement; | ||
} | ||
} | ||
function focusTabbableAfterTrigger(event) { | ||
var elementAfterTrigger = getElementAfterTrigger(); | ||
if (elementAfterTrigger) { | ||
event.preventDefault(); | ||
elementAfterTrigger.focus(); | ||
} | ||
} | ||
function shiftTabbedFromElementAfterTrigger(event) { | ||
if (!event.shiftKey) return; | ||
var elementAfterTrigger = getElementAfterTrigger(); | ||
return event.target === elementAfterTrigger; | ||
} | ||
function focusLastTabbableInPopover(event) { | ||
var elements = (0, _tabbable2.default)(popoverRef.current); | ||
var last = elements[elements.length - 1]; | ||
if (last) { | ||
event.preventDefault(); | ||
last.focus(); | ||
} | ||
} | ||
function shiftTabbedOutOfPopover(event) { | ||
var elements = (0, _tabbable2.default)(popoverRef.current); | ||
return elements.length === 0 ? false : event.target === elements[0]; | ||
} | ||
function focusTriggerRef(event) { | ||
event.preventDefault(); | ||
triggerRef.current.focus(); | ||
} | ||
function tabbedToBrowserChrome(event) { | ||
var elements = (0, _tabbable2.default)(doc).filter(function (element) { | ||
return !popoverRef.current.contains(element); | ||
}); | ||
return event.target === elements[elements.length - 1]; | ||
} | ||
function shiftTabbedToBrowserChrome(event) { | ||
// we're assuming the popover will never contain the first tabbable | ||
// element, and it better not, because the trigger needs to be tabbable! | ||
return event.target === (0, _tabbable2.default)(doc)[0]; | ||
} | ||
var restoreTabIndexTuplés = []; | ||
function disableTabbablesInPopover() { | ||
var elements = (0, _tabbable2.default)(popoverRef.current); | ||
elements.forEach(function (element) { | ||
restoreTabIndexTuplés.push([element, element.tabIndex]); | ||
element.tabIndex = -1; | ||
}); | ||
doc.addEventListener("focusin", enableTabbablesInPopover); | ||
} | ||
function enableTabbablesInPopover(event) { | ||
doc.removeEventListener("focusin", enableTabbablesInPopover); | ||
restoreTabIndexTuplés.forEach(function (_ref2) { | ||
var element = _ref2[0], | ||
tabIndex = _ref2[1]; | ||
element.tabIndex = tabIndex; | ||
}); | ||
} | ||
} |
{ | ||
"name": "@reach/popover", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "Render a portal positioned relative to another element.", | ||
@@ -15,3 +15,4 @@ "main": "index.js", | ||
"@reach/rect": "^0.2.1", | ||
"@reach/utils": "^0.2.3" | ||
"@reach/utils": "^0.2.3", | ||
"tabbable": "^4.0.0" | ||
}, | ||
@@ -18,0 +19,0 @@ "peerDependencies": { |
181
src/index.js
@@ -1,5 +0,6 @@ | ||
import React, { useRef, forwardRef } from "react"; | ||
import React, { useRef, forwardRef, useEffect } from "react"; | ||
import Portal from "@reach/portal"; | ||
import { useRect } from "@reach/rect"; | ||
import { assignRef } from "@reach/utils"; | ||
import tabbable from "tabbable"; | ||
@@ -20,5 +21,8 @@ export default forwardRef(function Popover(props, ref) { | ||
) { | ||
const popupRef = useRef(); | ||
const popupRect = useRect(popupRef); | ||
const popoverRef = useRef(); | ||
const popoverRect = useRect(popoverRef); | ||
const targetRect = useRect(targetRef); | ||
useSimulateTabNavigationForReactTree(targetRef, popoverRef); | ||
return ( | ||
@@ -28,3 +32,3 @@ <div | ||
ref={node => { | ||
assignRef(popupRef, node); | ||
assignRef(popoverRef, node); | ||
assignRef(forwardedRef, node); | ||
@@ -35,3 +39,3 @@ }} | ||
position: "absolute", | ||
...getStyles(position, targetRect, popupRect) | ||
...getStyles(position, targetRect, popoverRect) | ||
}} | ||
@@ -43,18 +47,21 @@ {...rest} | ||
const getStyles = (position, targetRect, popupRect) => { | ||
const needToMeasurePopup = !popupRect; | ||
const getStyles = (position, targetRect, popoverRect) => { | ||
const needToMeasurePopup = !popoverRect; | ||
if (needToMeasurePopup) { | ||
return { visibility: "hidden" }; | ||
} | ||
return position(targetRect, popupRect); | ||
return position(targetRect, popoverRect); | ||
}; | ||
export function positionDefault(targetRect, popupRect) { | ||
const { directionUp, directionRight } = getCollisions(targetRect, popupRect); | ||
export function positionDefault(targetRect, popoverRect) { | ||
const { directionUp, directionRight } = getCollisions( | ||
targetRect, | ||
popoverRect | ||
); | ||
return { | ||
left: directionRight | ||
? `${targetRect.right - popupRect.width + window.pageXOffset}px` | ||
? `${targetRect.right - popoverRect.width + window.pageXOffset}px` | ||
: `${targetRect.left + window.pageXOffset}px`, | ||
top: directionUp | ||
? `${targetRect.top - popupRect.height + window.pageYOffset}px` | ||
? `${targetRect.top - popoverRect.height + window.pageYOffset}px` | ||
: `${targetRect.top + targetRect.height + window.pageYOffset}px` | ||
@@ -64,4 +71,4 @@ }; | ||
export function positionMatchWidth(targetRect, popupRect) { | ||
const { directionUp } = getCollisions(targetRect, popupRect); | ||
export function positionMatchWidth(targetRect, popoverRect) { | ||
const { directionUp } = getCollisions(targetRect, popoverRect); | ||
return { | ||
@@ -71,3 +78,3 @@ width: targetRect.width, | ||
top: directionUp | ||
? `${targetRect.top - popupRect.height + window.pageYOffset}px` | ||
? `${targetRect.top - popoverRect.height + window.pageYOffset}px` | ||
: `${targetRect.top + targetRect.height + window.pageYOffset}px` | ||
@@ -78,9 +85,9 @@ }; | ||
// Finish this another time | ||
// export function positionHorizontalCenter(targetRect, popupRect) { | ||
// export function positionHorizontalCenter(targetRect, popoverRect) { | ||
// const targetCenter = targetRect.width / 2 + targetRect.left; | ||
// const popupHalf = popupRect.width / 2; | ||
// const popoverHalf = popoverRect.width / 2; | ||
// const collisions = { | ||
// right: window.innerWidth < targetCenter - popupHalf, | ||
// left: targetCenter - popupHalf < 0 | ||
// right: window.innerWidth < targetCenter - popoverHalf, | ||
// left: targetCenter - popoverHalf < 0 | ||
// // top: | ||
@@ -92,3 +99,3 @@ // // bottom: | ||
// left: collisions.right | ||
// ? `${targetRect.right - popupRect.width + window.pageXOffset}px` | ||
// ? `${targetRect.right - popoverRect.width + window.pageXOffset}px` | ||
// : collisions.left ? `` : `` | ||
@@ -100,3 +107,3 @@ // }; | ||
targetRect, | ||
popupRect, | ||
popoverRect, | ||
offsetLeft = 0, | ||
@@ -106,7 +113,8 @@ offsetBottom = 0 | ||
const collisions = { | ||
top: targetRect.top - popupRect.height < 0, | ||
right: window.innerWidth < targetRect.left + popupRect.width - offsetLeft, | ||
top: targetRect.top - popoverRect.height < 0, | ||
right: window.innerWidth < targetRect.left + popoverRect.width - offsetLeft, | ||
bottom: | ||
window.innerHeight < targetRect.bottom + popupRect.height - offsetBottom, | ||
left: targetRect.left - popupRect.width < 0 | ||
window.innerHeight < | ||
targetRect.bottom + popoverRect.height - offsetBottom, | ||
left: targetRect.left - popoverRect.width < 0 | ||
}; | ||
@@ -119,1 +127,124 @@ | ||
} | ||
// Heads up, my jQuery past haunts this function. This hook scopes the tab | ||
// order to the React element tree, instead of the DOM tree. This way, when the | ||
// user navigates with tab from the targetRef, the tab order moves into the | ||
// popup, and then out of the popup back to the rest of the document. | ||
// (We call targetRef, triggerRef inside this function to avoid confusion with | ||
// event.target) | ||
function useSimulateTabNavigationForReactTree(triggerRef, popoverRef) { | ||
const doc = triggerRef.current.ownerDocument; // maybe in devtools | ||
function handleKeyDown(event) { | ||
if (event.key === "Tab" && tabbable(popoverRef.current).length === 0) { | ||
return; | ||
} | ||
if (event.key === "Tab" && event.shiftKey) { | ||
if (shiftTabbedFromElementAfterTrigger(event)) { | ||
focusLastTabbableInPopover(event); | ||
} else if (shiftTabbedOutOfPopover(event)) { | ||
focusTriggerRef(event); | ||
} else if (shiftTabbedToBrowserChrome(event)) { | ||
disableTabbablesInPopover(event); | ||
} | ||
} else if (event.key === "Tab") { | ||
if (tabbedFromTriggerToPopover(event)) { | ||
focusFirstPopoverTabbable(event); | ||
} else if (tabbedOutOfPopover(event)) { | ||
focusTabbableAfterTrigger(event); | ||
} else if (tabbedToBrowserChrome(event)) { | ||
disableTabbablesInPopover(event); | ||
} | ||
} | ||
} | ||
useEffect(() => { | ||
doc.addEventListener("keydown", handleKeyDown); | ||
return () => doc.removeEventListener("keydown", handleKeyDown); | ||
}, []); | ||
function getElementAfterTrigger() { | ||
const elements = tabbable(doc); | ||
const targetIndex = elements.indexOf(triggerRef.current); | ||
return elements[targetIndex + 1]; | ||
} | ||
function tabbedFromTriggerToPopover() { | ||
return triggerRef.current === document.activeElement; | ||
} | ||
function focusFirstPopoverTabbable(event) { | ||
const elements = tabbable(popoverRef.current); | ||
if (elements[0]) { | ||
event.preventDefault(); | ||
elements[0].focus(); | ||
} | ||
} | ||
function tabbedOutOfPopover(event) { | ||
const inPopover = popoverRef.current.contains(document.activeElement); | ||
if (inPopover) { | ||
const elements = tabbable(popoverRef.current); | ||
return elements[elements.length - 1] === document.activeElement; | ||
} | ||
} | ||
function focusTabbableAfterTrigger(event) { | ||
const elementAfterTrigger = getElementAfterTrigger(); | ||
if (elementAfterTrigger) { | ||
event.preventDefault(); | ||
elementAfterTrigger.focus(); | ||
} | ||
} | ||
function shiftTabbedFromElementAfterTrigger(event) { | ||
if (!event.shiftKey) return; | ||
const elementAfterTrigger = getElementAfterTrigger(); | ||
return event.target === elementAfterTrigger; | ||
} | ||
function focusLastTabbableInPopover(event) { | ||
const elements = tabbable(popoverRef.current); | ||
const last = elements[elements.length - 1]; | ||
if (last) { | ||
event.preventDefault(); | ||
last.focus(); | ||
} | ||
} | ||
function shiftTabbedOutOfPopover(event) { | ||
const elements = tabbable(popoverRef.current); | ||
return elements.length === 0 ? false : event.target === elements[0]; | ||
} | ||
function focusTriggerRef(event) { | ||
event.preventDefault(); | ||
triggerRef.current.focus(); | ||
} | ||
function tabbedToBrowserChrome(event) { | ||
const elements = tabbable(doc).filter( | ||
element => !popoverRef.current.contains(element) | ||
); | ||
return event.target === elements[elements.length - 1]; | ||
} | ||
function shiftTabbedToBrowserChrome(event) { | ||
// we're assuming the popover will never contain the first tabbable | ||
// element, and it better not, because the trigger needs to be tabbable! | ||
return event.target === tabbable(doc)[0]; | ||
} | ||
let restoreTabIndexTuplés = []; | ||
function disableTabbablesInPopover() { | ||
const elements = tabbable(popoverRef.current); | ||
elements.forEach(element => { | ||
restoreTabIndexTuplés.push([element, element.tabIndex]); | ||
element.tabIndex = -1; | ||
}); | ||
doc.addEventListener("focusin", enableTabbablesInPopover); | ||
} | ||
function enableTabbablesInPopover(event) { | ||
doc.removeEventListener("focusin", enableTabbablesInPopover); | ||
restoreTabIndexTuplés.forEach(([element, tabIndex]) => { | ||
element.tabIndex = tabIndex; | ||
}); | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
58557
5
632
6
1
+ Addedtabbable@^4.0.0
+ Addedtabbable@4.0.0(transitive)