Skip to content

Instantly share code, notes, and snippets.

@ladifire
Created April 21, 2024 02:55
Show Gist options
  • Save ladifire/67ee14bcd34f9914c6bf86274819b88b to your computer and use it in GitHub Desktop.
Save ladifire/67ee14bcd34f9914c6bf86274819b88b to your computer and use it in GitHub Desktop.
PressableText.tsx - Rewritten by Cong Nguyen
// 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