Created
April 21, 2024 02:55
-
-
Save ladifire/67ee14bcd34f9914c6bf86274819b88b to your computer and use it in GitHub Desktop.
PressableText.tsx - Rewritten by Cong Nguyen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Rewritten by Cong Nguyen | |
// original code from Facebook Frontend website: https://gist.github.com/ladifire/21fb3e774cf62ac50d0700fd50d1ccb2 | |
import React, { useCallback, useContext, useRef, useState } from "react"; | |
import { PressableGroupContext } from "@facebook-frontend/context"; | |
import { joinClasses, useMergeRefs } from "@facebook-frontend/utils"; | |
import stylex from "@stylexjs/stylex"; | |
import { Pressability } from "./Pressability"; | |
import { useWebPressableTouchStartHandler } from "./useWebPressableTouchStartHandler"; | |
// TODO: try to understand these unknown constants | |
const justknobx_450 = true; | |
const gkx_21059 = false; | |
const styles = stylex.create({ | |
disabled: { | |
cursor: "not-allowed", | |
}, | |
focusNotVisible: { | |
outline: "none", | |
}, | |
linkFocusRing: { | |
outline: "var(--focus-ring-outline-link)", | |
}, | |
notSelectable: { | |
userSelect: "none", | |
}, | |
root: { | |
WebkitTapHighlightColor: "transparent", | |
backgroundColor: "transparent", | |
borderTop: 0, | |
borderEnd: 0, | |
borderBottom: 0, | |
borderStart: 0, | |
boxSizing: "border-box", | |
cursor: "pointer", | |
display: "inline", | |
listStyle: "none", | |
marginTop: 0, | |
marginEnd: 0, | |
marginBottom: 0, | |
marginStart: 0, | |
paddingTop: 0, | |
paddingEnd: 0, | |
paddingBottom: 0, | |
paddingStart: 0, | |
textAlign: "inherit", | |
textDecoration: "none", | |
touchAction: "manipulation", | |
}, | |
rootInGroup: { | |
touchAction: "none", | |
}, | |
}); | |
const ACCESSIBILITY_ROLES = ["menuitem", "tab", "none"]; | |
const DEFAULT_TAGS = { | |
article: "article", | |
banner: "header", | |
complementary: "aside", | |
contentinfo: "footer", | |
figure: "figure", | |
form: "form", | |
heading: "h1", | |
label: "label", | |
link: "a", | |
list: "ul", | |
listitem: "li", | |
main: "main", | |
navigation: "nav", | |
none: "div", | |
region: "section", | |
}; | |
const getLinkTag = (accessibilityRole, link) => { | |
var tag = "div"; | |
if ( | |
link?.url !== "#" || | |
(ACCESSIBILITY_ROLES.includes(accessibilityRole) && link?.url) | |
) { | |
tag = "a"; | |
} else if (accessibilityRole) { | |
if (DEFAULT_TAGS[accessibilityRole]) { | |
tag = DEFAULT_TAGS[accessibilityRole]; | |
} | |
} | |
return tag; | |
}; | |
// TODO: refactor this function name | |
const __r = (event) => { | |
let target = event.target; | |
let tagName = target.tagName; | |
let focusable = | |
target.isContentEditable || | |
(tagName === "A" && target.href != null) || | |
tagName === "BUTTON" || | |
tagName === "INPUT" || | |
tagName === "SELECT" || | |
tagName === "TEXTAREA"; | |
if (target.tabIndex === 0 && !focusable) { | |
let key = event.key; | |
if (key === "Enter") return true; | |
let role = target.getAttribute("role"); | |
if ( | |
(key === " " || key === "Spacebar") && | |
(role === "button" || | |
role === "combobox" || | |
role === "menuitem" || | |
role === "menuitemradio" || | |
role === "option") | |
) | |
return true; | |
} | |
return false; | |
}; | |
const hasNode = (node) => { | |
return typeof document !== "undefined" && | |
typeof document.contains === "function" | |
? document.contains(node) | |
: false; | |
}; | |
const hasLinkHref = (node) => { | |
let currentNode = node; | |
while (currentNode) { | |
if (currentNode.tagName === "A" && currentNode.href) { | |
return true; | |
} | |
currentNode = currentNode.parentNode; | |
} | |
return false; | |
}; | |
const shouldPreventDefault = (event, preventDefault) => { | |
let { altKey, ctrlKey, currentTarget, metaKey, shiftKey, target } = event; | |
let _target; | |
justknobx_450 && (_target = hasNode(target) ? target : currentTarget); | |
target = hasLinkHref(_target); | |
currentTarget = altKey || ctrlKey || metaKey || shiftKey; | |
return preventDefault !== false && target && !currentTarget; | |
}; | |
export const PressableText = (props: any) => { | |
let { | |
accessibilityLabel, | |
accessibilityRelationship, | |
accessibilityRole, | |
accessibilityState, | |
children, | |
className_DEPRECATED, | |
direction, | |
disabled, | |
focusable, | |
forwardedRef, | |
link, | |
nativeID, | |
onBlur, | |
onContextMenu, | |
onFocus, | |
onFocusChange, | |
onFocusVisibleChange, | |
onHoverChange, | |
onHoverEnd, | |
onHoverMove, | |
onHoverStart, | |
onPress, | |
onPressChange, | |
onPressEnd, | |
onPressMove, | |
onPressStart, | |
preventContextMenu, | |
preventDefault = true, | |
selectable, | |
style, | |
suppressFocusRing, | |
testID, | |
testOnly_state, | |
xstyle, | |
...rest | |
} = props; | |
const ref = useRef(null); | |
const [focused, setFocused] = useState(false); | |
const [focusVisible, setFocusVisible] = useState(false); | |
const [hovered, setHovered] = useState(false); | |
const [pressed, setPressed] = useState(false); | |
const pressableGroupContext = useContext(PressableGroupContext); | |
const Component = getLinkTag(accessibilityRole, link); | |
disabled = disabled || accessibilityRole?.disabled; | |
const hasLink = Component === "a" && !disabled; | |
// TODO: refactor this variable name | |
const f = { | |
disabled: disabled === true || testOnly_state?.disabled === true || false, | |
focused: focused || testOnly_state?.focused === true, | |
focusVisible: | |
(focusVisible && suppressFocusRing !== true) || | |
testOnly_state?.focusVisible === true, | |
hovered: hovered || testOnly_state?.hovered === true, | |
pressed: pressed || testOnly_state?.pressed === true, | |
}; | |
const _children = typeof children === "function" ? children(f) : children; | |
let _classname = | |
typeof className_DEPRECATED === "function" | |
? className_DEPRECATED(f) | |
: className_DEPRECATED; | |
let _xstyle = typeof xstyle === "function" ? xstyle(f) : xstyle; | |
let _style = typeof style === "function" ? style(f) : style; | |
Pressability.usePressability(ref, { | |
disabled, | |
onBlur, | |
onContextMenu, | |
onFocus, | |
onFocusChange: handleChangeValue(setFocused, onFocusChange), | |
onFocusVisibleChange: handleChangeValue( | |
setFocusVisible, | |
onFocusVisibleChange | |
), | |
onHoverChange: handleChangeValue(setHovered, onHoverChange), | |
onHoverEnd, | |
onHoverMove, | |
onHoverStart, | |
onPressChange: handleChangeValue(setPressed, onPressChange), | |
onPressEnd, | |
onPressMove, | |
onPressStart, | |
preventContextMenu, | |
preventDefault, | |
}); | |
const handleClick = useCallback( | |
(event) => { | |
if (onPress) { | |
onPress(event); | |
} | |
if (onPress || link) { | |
event.stopPropagation(); | |
} | |
if (shouldPreventDefault(event, preventDefault)) { | |
event.nativeEvent.preventDefault(); | |
} | |
}, | |
[link, onPress, preventDefault] | |
); | |
const handleKeyDown = useCallback( | |
(event) => { | |
if (__r(event)) { | |
(event.key === " " || event.key === "Spacebar") && | |
event.preventDefault(); | |
if (onPress) { | |
onPress(event); | |
event.stopPropagation(); | |
} | |
} | |
}, | |
[onPress] | |
); | |
let _direction; | |
switch (direction) { | |
case "none": | |
break; | |
default: | |
if (direction) { | |
_direction = direction; | |
} | |
break; | |
} | |
const combinedRef = useMergeRefs(ref, forwardedRef); | |
useWebPressableTouchStartHandler(ref, pressableGroupContext, handleClick); | |
const role = | |
accessibilityRole === "none" ? "presentation" : accessibilityRole; | |
let tabIndex; | |
let pressable = Component === "a" || accessibilityRole === "button"; | |
let hidden = accessibilityState?.hidden; | |
if (pressable) { | |
if ( | |
hidden === true || | |
focusable === false || | |
(!gkx_21059 && disabled === true) | |
) { | |
tabIndex = -1; | |
} else { | |
tabIndex = 0; | |
} | |
} else if (gkx_21059) { | |
if ( | |
hidden !== true && | |
focusable !== false && | |
accessibilityRole !== "none" | |
) { | |
tabIndex = 0; | |
} | |
} else if ( | |
disabled !== true && | |
hidden !== true && | |
focusable !== false && | |
accessibilityRole !== "none" | |
) { | |
tabIndex = 0; | |
} | |
let canDownload = | |
(link?.download === true || typeof link?.download === "string") && hasLink; | |
return ( | |
<Component | |
{...Object.assign({}, rest, { | |
ref: combinedRef, | |
"aria-activedescendant": accessibilityRelationship?.activedescendant, | |
"aria-busy": accessibilityState?.busy, | |
"aria-controls": accessibilityRelationship?.controls, | |
"aria-current": accessibilityRelationship?.current, | |
"aria-describedby": accessibilityRelationship?.describedby, | |
"aria-details": accessibilityRelationship?.details, | |
"aria-disabled": | |
disabled === !0 && role !== "presentation" ? disabled : undefined, | |
"aria-expanded": accessibilityState?.expanded, | |
"aria-haspopup": accessibilityRelationship?.haspopup, | |
"aria-hidden": accessibilityState?.hidden, | |
"aria-invalid": accessibilityState?.invalid, | |
"aria-label": accessibilityLabel, | |
"aria-labelledby": accessibilityRelationship?.labelledby, | |
"aria-owns": accessibilityRelationship?.owns, | |
"aria-pressed": accessibilityState?.pressed, | |
"aria-readonly": accessibilityState?.readonly, | |
"aria-required": accessibilityState?.required, | |
"aria-selected": accessibilityState?.selected, | |
attributionsrc: hasLink && link?.attributionsrc, | |
children: _children, | |
className: joinClasses( | |
stylex( | |
styles.root, | |
selectable === false && styles.notSelectable, | |
f.disabled && styles.disabled, | |
!f.focusVisible && styles.focusNotVisible, | |
f.focusVisible && pressable && styles.linkFocusRing, | |
_xstyle, | |
pressableGroupContext && styles.rootInGroup | |
), | |
_classname | |
), | |
dir: _direction, | |
download: canDownload ? link?.download : undefined, | |
href: hasLink && link?.url, | |
id: nativeID, | |
onClick: disabled ? undefined : handleClick, | |
onKeyDown: disabled ? undefined : handleKeyDown, | |
rel: hasLink && link?.rel, | |
role, | |
style: _style, | |
tabIndex, | |
target: hasLink && link?.target, | |
})} | |
/> | |
); | |
}; | |
const handleChangeValue = (handleChange, onChange) => { | |
return useCallback( | |
(newValue) => { | |
handleChange(newValue); | |
if (onChange) { | |
onChange(newValue); | |
} | |
}, | |
[onChange, handleChange] | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment