Skip to content

chore: Pass through more DOM events and attributes #8327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions packages/@react-aria/utils/src/filterDOMProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {AriaLabelingProps, DOMProps, LinkDOMProps} from '@react-types/shared';
import {AriaLabelingProps, DOMProps, GlobalDOMAttributes, LinkDOMProps} from '@react-types/shared';

const DOMPropNames = new Set([
'id'
Expand All @@ -34,13 +34,62 @@ const linkPropNames = new Set([
'referrerPolicy'
]);

const globalAttrs = new Set([
'dir',
'lang',
'hidden',
'inert',
'translate'
]);

const globalEvents = new Set([
'onClick',
'onAuxClick',
'onContextMenu',
'onDoubleClick',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onTouchCancel',
'onTouchEnd',
'onTouchMove',
'onTouchStart',
'onPointerDown',
'onPointerMove',
'onPointerUp',
'onPointerCancel',
'onPointerEnter',
'onPointerLeave',
'onPointerOver',
'onPointerOut',
'onGotPointerCapture',
'onLostPointerCapture',
'onScroll',
'onWheel',
'onAnimationStart',
'onAnimationEnd',
'onAnimationIteration',
'onTransitionCancel',
'onTransitionEnd',
'onTransitionRun',
'onTransitionStart'
]);

interface Options {
/**
* If labelling associated aria properties should be included in the filter.
*/
labelable?: boolean,
/** Whether the element is a link and should include DOM props for <a> elements. */
isLink?: boolean,
/** Whether to include global DOM attributes. */
global?: boolean,
/** Whether to include DOM events. */
events?: boolean,
/**
* A Set of other property names that should be included in the filter.
*/
Expand All @@ -54,8 +103,8 @@ const propRe = /^(data-.*)$/;
* @param props - The component props to be filtered.
* @param opts - Props to override.
*/
export function filterDOMProps(props: DOMProps & AriaLabelingProps & LinkDOMProps, opts: Options = {}): DOMProps & AriaLabelingProps {
let {labelable, isLink, propNames} = opts;
export function filterDOMProps(props: DOMProps & AriaLabelingProps & LinkDOMProps & GlobalDOMAttributes, opts: Options = {}): DOMProps & AriaLabelingProps {
let {labelable, isLink, global, events = global, propNames} = opts;
let filteredProps = {};

for (const prop in props) {
Expand All @@ -64,6 +113,8 @@ export function filterDOMProps(props: DOMProps & AriaLabelingProps & LinkDOMProp
DOMPropNames.has(prop) ||
(labelable && labelablePropNames.has(prop)) ||
(isLink && linkPropNames.has(prop)) ||
(global && globalAttrs.has(prop)) ||
(events && globalEvents.has(prop) || (prop.endsWith('Capture') && globalEvents.has(prop.slice(0, -7)))) ||
propNames?.has(prop) ||
propRe.test(prop)
)
Expand Down
113 changes: 112 additions & 1 deletion packages/@react-types/shared/src/dom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import {
AnimationEventHandler,
AriaAttributes,
AriaRole,
ClipboardEventHandler,
Expand All @@ -19,8 +20,14 @@ import {
FormEventHandler,
HTMLAttributeAnchorTarget,
HTMLAttributeReferrerPolicy,
MouseEventHandler,
PointerEventHandler,
DOMAttributes as ReactDOMAttributes,
ReactEventHandler
ReactEventHandler,
TouchEventHandler,
TransitionEventHandler,
UIEventHandler,
WheelEventHandler
} from 'react';

export interface AriaLabelingProps {
Expand Down Expand Up @@ -223,3 +230,107 @@ export interface DOMAttributes<T = FocusableElement> extends AriaAttributes, Rea
export interface GroupDOMAttributes extends Omit<DOMAttributes<HTMLElement>, 'role'> {
role?: 'group' | 'region' | 'presentation'
}

/**
* Global attributes that can be applied to any DOM element.
* @private
*/
// NOTE: id is handled elsewhere (DOMProps).
export interface GlobalDOMAttributes<T = Element> extends GlobalDOMEvents<T> {
dir?: string | undefined,
lang?: string | undefined,
hidden?: boolean | undefined,
inert?: boolean | undefined,
translate?: 'yes' | 'no' | undefined
}

/**
* Global DOM events that are supported on all DOM elements.
* @private
*/
// NOTES:
// - Drag and drop events are omitted for now.
// - Keyboard and focus events are supported directly on focusable elements (FocusableProps).
// - Text input events (e.g. onInput, onCompositionStart, onCopy) are
// supported only directly on input elements (TextInputDOMProps).
// We don't support contentEditable on our components.
// - Media events should be handled directly on the <video>/<audio><img> element.
export interface GlobalDOMEvents<T = Element> {
// MouseEvents
onClick?: MouseEventHandler<T> | undefined,
onClickCapture?: MouseEventHandler<T> | undefined,
onAuxClick?: MouseEventHandler<T> | undefined,
onAuxClickCapture?: MouseEventHandler<T> | undefined,
onContextMenu?: MouseEventHandler<T> | undefined,
onContextMenuCapture?: MouseEventHandler<T> | undefined,
onDoubleClick?: MouseEventHandler<T> | undefined,
onDoubleClickCapture?: MouseEventHandler<T> | undefined,
onMouseDown?: MouseEventHandler<T> | undefined,
onMouseDownCapture?: MouseEventHandler<T> | undefined,
onMouseEnter?: MouseEventHandler<T> | undefined,
onMouseLeave?: MouseEventHandler<T> | undefined,
onMouseMove?: MouseEventHandler<T> | undefined,
onMouseMoveCapture?: MouseEventHandler<T> | undefined,
onMouseOut?: MouseEventHandler<T> | undefined,
onMouseOutCapture?: MouseEventHandler<T> | undefined,
onMouseOver?: MouseEventHandler<T> | undefined,
onMouseOverCapture?: MouseEventHandler<T> | undefined,
onMouseUp?: MouseEventHandler<T> | undefined,
onMouseUpCapture?: MouseEventHandler<T> | undefined,

// Touch Events
onTouchCancel?: TouchEventHandler<T> | undefined,
onTouchCancelCapture?: TouchEventHandler<T> | undefined,
onTouchEnd?: TouchEventHandler<T> | undefined,
onTouchEndCapture?: TouchEventHandler<T> | undefined,
onTouchMove?: TouchEventHandler<T> | undefined,
onTouchMoveCapture?: TouchEventHandler<T> | undefined,
onTouchStart?: TouchEventHandler<T> | undefined,
onTouchStartCapture?: TouchEventHandler<T> | undefined,

// Pointer Events
onPointerDown?: PointerEventHandler<T> | undefined,
onPointerDownCapture?: PointerEventHandler<T> | undefined,
onPointerMove?: PointerEventHandler<T> | undefined,
onPointerMoveCapture?: PointerEventHandler<T> | undefined,
onPointerUp?: PointerEventHandler<T> | undefined,
onPointerUpCapture?: PointerEventHandler<T> | undefined,
onPointerCancel?: PointerEventHandler<T> | undefined,
onPointerCancelCapture?: PointerEventHandler<T> | undefined,
onPointerEnter?: PointerEventHandler<T> | undefined,
onPointerLeave?: PointerEventHandler<T> | undefined,
onPointerOver?: PointerEventHandler<T> | undefined,
onPointerOverCapture?: PointerEventHandler<T> | undefined,
onPointerOut?: PointerEventHandler<T> | undefined,
onPointerOutCapture?: PointerEventHandler<T> | undefined,
onGotPointerCapture?: PointerEventHandler<T> | undefined,
onGotPointerCaptureCapture?: PointerEventHandler<T> | undefined,
onLostPointerCapture?: PointerEventHandler<T> | undefined,
onLostPointerCaptureCapture?: PointerEventHandler<T> | undefined,

// UI Events
onScroll?: UIEventHandler<T> | undefined,
onScrollCapture?: UIEventHandler<T> | undefined,

// Wheel Events
onWheel?: WheelEventHandler<T> | undefined,
onWheelCapture?: WheelEventHandler<T> | undefined,

// Animation Events
onAnimationStart?: AnimationEventHandler<T> | undefined,
onAnimationStartCapture?: AnimationEventHandler<T> | undefined,
onAnimationEnd?: AnimationEventHandler<T> | undefined,
onAnimationEndCapture?: AnimationEventHandler<T> | undefined,
onAnimationIteration?: AnimationEventHandler<T> | undefined,
onAnimationIterationCapture?: AnimationEventHandler<T> | undefined,

// Transition Events
onTransitionCancel?: TransitionEventHandler<T> | undefined,
onTransitionCancelCapture?: TransitionEventHandler<T> | undefined,
onTransitionEnd?: TransitionEventHandler<T> | undefined,
onTransitionEndCapture?: TransitionEventHandler<T> | undefined,
onTransitionRun?: TransitionEventHandler<T> | undefined,
onTransitionRunCapture?: TransitionEventHandler<T> | undefined,
onTransitionStart?: TransitionEventHandler<T> | undefined,
onTransitionStartCapture?: TransitionEventHandler<T> | undefined
}
8 changes: 7 additions & 1 deletion packages/dev/parcel-transformer-docs/DocsTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,14 +376,20 @@ module.exports = new Transformer({
let exts = path.node.extends ? path.get('extends').map(e => processExport(e)) : [];
let docs = getJSDocs(path);

return Object.assign(node, addDocs({
let res = Object.assign(node, addDocs({
type: 'interface',
id: `${asset.filePath}:${path.node.id.name}`,
name: path.node.id.name,
extends: exts,
properties,
typeParameters: path.node.typeParameters ? path.get('typeParameters.params').map(p => processExport(p)) : []
}, docs));
if (res.access === 'private') {
for (let prop in res.properties) {
res.properties[prop].access = 'private';
}
}
return res;
}

if (path.isTSTypeLiteral()) {
Expand Down
16 changes: 10 additions & 6 deletions packages/react-aria-components/src/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import {AriaBreadcrumbsProps, useBreadcrumbs} from 'react-aria';
import {Collection, CollectionBuilder, createLeafComponent} from '@react-aria/collections';
import {CollectionProps, CollectionRendererContext} from './Collection';
import {ContextValue, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps, useSlottedContext} from './utils';
import {filterDOMProps} from '@react-aria/utils';
import {forwardRefType, Key} from '@react-types/shared';
import {filterDOMProps, mergeProps} from '@react-aria/utils';
import {forwardRefType, GlobalDOMAttributes, Key} from '@react-types/shared';
import {LinkContext} from './Link';
import {Node} from 'react-stately';
import React, {createContext, ForwardedRef, forwardRef, useContext} from 'react';

export interface BreadcrumbsProps<T> extends Omit<CollectionProps<T>, 'disabledKeys'>, AriaBreadcrumbsProps, StyleProps, SlotProps {
export interface BreadcrumbsProps<T> extends Omit<CollectionProps<T>, 'disabledKeys'>, AriaBreadcrumbsProps, StyleProps, SlotProps, GlobalDOMAttributes<HTMLOListElement> {
/** Whether the breadcrumbs are disabled. */
isDisabled?: boolean,
/** Handler that is called when a breadcrumb is clicked. */
Expand All @@ -35,13 +35,14 @@ export const Breadcrumbs = /*#__PURE__*/ (forwardRef as forwardRefType)(function
[props, ref] = useContextProps(props, ref, BreadcrumbsContext);
let {CollectionRoot} = useContext(CollectionRendererContext);
let {navProps} = useBreadcrumbs(props);
let DOMProps = filterDOMProps(props, {global: true});

return (
<CollectionBuilder content={<Collection {...props} />}>
{collection => (
<ol
ref={ref}
{...navProps}
{...mergeProps(DOMProps, navProps)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i assume we should merge ours last always so we win on important attributes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I tried to do that. The events won't conflict (they'll merge) but other ones like inert would.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking of inert, shouldn't FOCUSABLE_ELEMENT_SELECTOR skip over inert elements?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slot={props.slot || undefined}
style={props.style}
className={props.className ?? 'react-aria-Breadcrumbs'}>
Expand All @@ -67,7 +68,7 @@ export interface BreadcrumbRenderProps {
isDisabled: boolean
}

export interface BreadcrumbProps extends RenderProps<BreadcrumbRenderProps> {
export interface BreadcrumbProps extends RenderProps<BreadcrumbRenderProps>, GlobalDOMAttributes<HTMLLIElement> {
/** A unique id for the breadcrumb, which will be passed to `onAction` when the breadcrumb is pressed. */
id?: Key
}
Expand All @@ -92,9 +93,12 @@ export const Breadcrumb = /*#__PURE__*/ createLeafComponent('item', function Bre
defaultClassName: 'react-aria-Breadcrumb'
});

let DOMProps = filterDOMProps(props as any, {global: true});
delete DOMProps.id;

return (
<li
{...filterDOMProps(props as any)}
{...DOMProps}
{...renderProps}
ref={ref}
data-disabled={isDisabled || isCurrent || undefined}
Expand Down
13 changes: 7 additions & 6 deletions packages/react-aria-components/src/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from './utils';
import {createHideableComponent} from '@react-aria/collections';
import {filterDOMProps} from '@react-aria/utils';
import {GlobalDOMAttributes} from '@react-types/shared';
import {ProgressBarContext} from './ProgressBar';
import React, {createContext, ForwardedRef, useEffect, useRef} from 'react';

Expand Down Expand Up @@ -65,7 +66,7 @@ export interface ButtonRenderProps {
isPending: boolean
}

export interface ButtonProps extends Omit<AriaButtonProps, 'children' | 'href' | 'target' | 'rel' | 'elementType'>, HoverEvents, SlotProps, RenderProps<ButtonRenderProps> {
export interface ButtonProps extends Omit<AriaButtonProps, 'children' | 'href' | 'target' | 'rel' | 'elementType'>, HoverEvents, SlotProps, RenderProps<ButtonRenderProps>, Omit<GlobalDOMAttributes<HTMLButtonElement>, 'onClick'> {
/**
* The `<form>` element to associate the button with.
* The value of this attribute must be the id of a `<form>` in the same document.
Expand Down Expand Up @@ -157,13 +158,13 @@ export const Button = /*#__PURE__*/ createHideableComponent(function Button(prop
wasPending.current = isPending;
}, [isPending, isFocused, ariaLabelledby, buttonId]);

// When the button is in a pending state, we want to stop implicit form submission (ie. when the user presses enter on a text input).
// We do this by changing the button's type to button.
let DOMProps = filterDOMProps(props, {global: true, propNames: additionalButtonHTMLAttributes});

return (
<button
{...filterDOMProps(props, {propNames: additionalButtonHTMLAttributes})}
{...mergeProps(buttonProps, focusProps, hoverProps)}
{...renderProps}
{...mergeProps(DOMProps, renderProps, buttonProps, focusProps, hoverProps)}
// When the button is in a pending state, we want to stop implicit form submission (ie. when the user presses enter on a text input).
// We do this by changing the button's type to button.
type={buttonProps.type === 'submit' && isPending ? 'button' : buttonProps.type}
id={buttonId}
ref={ref}
Expand Down
Loading