Created
June 13, 2021 20:37
-
-
Save kfranqueiro/ef43561453440ae29a42fe22a1ed4a8a to your computer and use it in GitHub Desktop.
Implementation of https://inclusive-components.design/cards/ (using Chakra UI, but shouldn't be hard to migrate to something else) - demo at https://codesandbox.io/s/inclusive-components-cards-using-react-chakra-ui-vq9mm
This file contains 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
import * as React from "react"; | |
import { BoxProps, CSSObject } from "@chakra-ui/react"; | |
interface Options { | |
/** Distance (in px) under which to still consider interaction a click */ | |
threshold?: number; | |
} | |
/** | |
* Returns mousedown and mouseup handlers to process clicks on an ancestor of the | |
* intended click target (with the intent of avoiding triggering on text selection). | |
* Useful for interactive cards with either one or no visible child CTA. | |
* | |
* Intended to be used together with generateContainerBoxProps spread on the same ancestor element, | |
* and generateTargetStyles spread on a child CTA or click target. | |
* | |
* You may be wondering, "why not just make the top-level container a link or button?" | |
* There are several reasons, including: | |
* * Assistive technology will try to announce the entire card’s contents before indicating it’s a button or link | |
* * In the case of buttons, you lose any semantic meaning of child elements, and are restricted to only phrasing elements | |
* | |
* Based on guidance seen here: https://inclusive-components.design/cards/ | |
* | |
* Example usage: | |
* | |
* ```tsx | |
* const [target, setTarget] = React.useState(null); | |
* const targetRef = React.useCallback(node => setTarget(node), [setTarget]); | |
* <Box | |
* {...{ | |
* ...useAccessibleClickHandlers(target), | |
* ...generateContainerBoxProps({ boxShadow: 'card-focus' }, { boxShadow: 'card-hover' }), | |
* }} | |
* > | |
* This is a card with a single CTA. | |
* <Button | |
* ref={targetRef} | |
* onClick={handleClick} | |
* {...generateTargetStyles()} | |
* > | |
* Perform the action associated with this card | |
* </Button> | |
* </Box> | |
* ``` | |
* | |
* You can also use this pattern with a visually-hidden CTA by wrapping the child | |
* button or link with Chakra UI's `VisuallyHidden` component. | |
* | |
* @param target Element to forward click to | |
* @param options | |
*/ | |
export function useAccessibleClickHandlers(target: HTMLElement | null, options?: Options) { | |
const pos = React.useRef<{ x: number; y: number }>({ | |
x: Infinity, | |
y: Infinity, | |
}); | |
const threshold = options?.threshold || 8; | |
const onMouseDown = React.useCallback( | |
(event: React.MouseEvent<HTMLElement>) => { | |
// Set up to potentially fire event only for primary button click on element outside the actual trigger element | |
if (event.button === 0 && !target?.contains(event.target as Node)) { | |
pos.current = { x: event.clientX, y: event.clientY }; | |
} | |
}, | |
[target] | |
); | |
const onMouseUp = React.useCallback( | |
(event: React.MouseEvent) => { | |
// Check based on distance rather than time, since someone could | |
// hold down the mouse button for a while before being sure they want to click | |
if ( | |
Math.abs(event.clientX - pos.current.x) < threshold && | |
Math.abs(event.clientY - pos.current.y) < threshold | |
) { | |
target?.focus(); | |
target?.click(); | |
} | |
}, | |
[target, threshold] | |
); | |
return target | |
? { | |
onMouseDown, | |
onMouseUp, | |
} | |
: {}; | |
} | |
/** | |
* Generates Box props applicable to a clickable ancestor container (e.g. a card). | |
* | |
* Intended to be used together with useAccessibleClickHandlers spread on the same ancestor component, | |
* and generateTargetStyles spread on a child CTA or click target. | |
* | |
* Based on guidance seen here: https://inclusive-components.design/cards/ | |
* | |
* @param focusStyles Styles to apply to the container during focus state. | |
* @param hoverStyles Styles to apply to the container during hover state. | |
*/ | |
export function generateContainerBoxProps( | |
focusStyles: CSSObject, | |
hoverStyles: CSSObject | |
): BoxProps { | |
return { | |
cursor: "pointer", | |
_hover: hoverStyles, | |
// Note: _focusWithin is intentionally after _hover to take precedence if they conflict | |
_focusWithin: focusStyles, | |
}; | |
} | |
/** | |
* Generates styles applicable to the child intended to be the click target in a clickable ancestor container. | |
* | |
* Intended to be used together with useAccessibleClickHandlers and generateContainerBoxProps spread on an | |
* ancestor component. | |
* | |
* This primarily supports cases where there is a single specific CTA displayed within e.g. a card but we want | |
* the entire card to be clickable. It can also be used in cases where no CTA should be visible, by wrapping | |
* the element in question with Chakra's VisuallyHidden component. | |
* | |
* Based on guidance seen here: https://inclusive-components.design/cards/ | |
*/ | |
export function generateTargetStyles() { | |
return { | |
// This is intentional, paired with passing styles to generateContainerBoxProps for :focus-within | |
_focus: { outline: "none" } as const, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment