/** * Copyright (c) Nicolas Gallagher. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @noflow */ import { getCSSStylesAtomic, type StyleObject } from '@tamagui/web' import { AccessibilityUtil } from '../AccessibilityUtil/index' const emptyObject = {} const hasOwnProperty = Object.prototype.hasOwnProperty const isArray = Array.isArray // react-native props that should be stripped before reaching the DOM const reactNativeOnlyProps: Record = { collapsable: true, contentContainerStyle: true, contentOffset: true, decelerationRate: true, maintainVisibleContentPosition: true, onLayout: true, onMomentumScrollBegin: true, onMomentumScrollEnd: true, onMoveShouldSetResponder: true, onMoveShouldSetResponderCapture: true, onResponderEnd: true, onResponderGrant: true, onResponderMove: true, onResponderReject: true, onResponderRelease: true, onResponderStart: true, onResponderTerminate: true, onResponderTerminationRequest: true, onScrollBeginDrag: true, onScrollEndDrag: true, onScrollShouldSetResponder: true, onScrollShouldSetResponderCapture: true, onSelectionChangeShouldSetResponder: true, onSelectionChangeShouldSetResponderCapture: true, onStartShouldSetResponder: true, onStartShouldSetResponderCapture: true, refreshControl: true, removeClippedSubviews: true, scrollEnabled: true, scrollEventThrottle: true, scrollIndicatorInsets: true, showsHorizontalScrollIndicator: true, showsVerticalScrollIndicator: true, snapToAlignment: true, snapToEnd: true, snapToInterval: true, snapToOffsets: true, snapToStart: true, stickyHeaderIndices: true, ScrollComponent: true, } const uppercasePattern = /[A-Z]/g function toHyphenLower(match) { return '-' + match.toLowerCase() } function hyphenateString(str: string): string { return str.replace(uppercasePattern, toHyphenLower) } function processIDRefList(idRefList: string | Array): string { return isArray(idRefList) ? idRefList.join(' ') : idRefList } function flattenStyle(style: any): any { if (style === null || typeof style !== 'object') { return undefined } if (!isArray(style)) { return style } const result = {} for (let i = 0, styleLength = style.length; i < styleLength; ++i) { const computedStyle = flattenStyle(style[i]) if (computedStyle) { for (const key in computedStyle) { if (hasOwnProperty.call(computedStyle, key)) { result[key] = computedStyle[key] } } } } return result } let pointerEventsStyles export const stylesFromProps = new WeakMap() export const createDOMProps = (elementType, props, options?) => { if (!props) { props = emptyObject } const { accessibilityActiveDescendant, accessibilityAtomic, accessibilityAutoComplete, accessibilityBusy, accessibilityChecked, accessibilityColumnCount, accessibilityColumnIndex, accessibilityColumnSpan, accessibilityControls, accessibilityCurrent, accessibilityDescribedBy, accessibilityDetails, accessibilityDisabled, accessibilityErrorMessage, accessibilityExpanded, accessibilityFlowTo, accessibilityHasPopup, accessibilityHidden, accessibilityInvalid, accessibilityKeyShortcuts, accessibilityLabel, accessibilityLabelledBy, accessibilityLevel, accessibilityLiveRegion, accessibilityModal, accessibilityMultiline, accessibilityMultiSelectable, accessibilityOrientation, accessibilityOwns, accessibilityPlaceholder, accessibilityPosInSet, accessibilityPressed, accessibilityReadOnly, accessibilityRequired, /* eslint-disable */ accessibilityRole, /* eslint-enable */ accessibilityRoleDescription, accessibilityRowCount, accessibilityRowIndex, accessibilityRowSpan, accessibilitySelected, accessibilitySetSize, accessibilitySort, accessibilityValueMax, accessibilityValueMin, accessibilityValueNow, accessibilityValueText, dataSet, focusable, nativeID, pointerEvents, style, testID, id, // Rest ...domProps } = props // strip react-native-only props that shouldn't reach the DOM for (const key in domProps) { if (reactNativeOnlyProps[key]) { delete domProps[key] } } const disabled = accessibilityDisabled const role = AccessibilityUtil.propsToAriaRole(props) // ACCESSIBILITY if (accessibilityActiveDescendant != null) { domProps['aria-activedescendant'] = accessibilityActiveDescendant } if (accessibilityAtomic != null) { domProps['aria-atomic'] = accessibilityAtomic } if (accessibilityAutoComplete != null) { domProps['aria-autocomplete'] = accessibilityAutoComplete } if (accessibilityBusy != null) { domProps['aria-busy'] = accessibilityBusy } if (accessibilityChecked != null) { domProps['aria-checked'] = accessibilityChecked } if (accessibilityColumnCount != null) { domProps['aria-colcount'] = accessibilityColumnCount } if (accessibilityColumnIndex != null) { domProps['aria-colindex'] = accessibilityColumnIndex } if (accessibilityColumnSpan != null) { domProps['aria-colspan'] = accessibilityColumnSpan } if (accessibilityControls != null) { domProps['aria-controls'] = processIDRefList(accessibilityControls) } if (accessibilityCurrent != null) { domProps['aria-current'] = accessibilityCurrent } if (accessibilityDescribedBy != null) { domProps['aria-describedby'] = processIDRefList(accessibilityDescribedBy) } if (accessibilityDetails != null) { domProps['aria-details'] = accessibilityDetails } if (disabled === true) { domProps['aria-disabled'] = true // Enhance with native semantics if ( elementType === 'button' || elementType === 'form' || elementType === 'input' || elementType === 'select' || elementType === 'textarea' ) { domProps.disabled = true } } if (accessibilityErrorMessage != null) { domProps['aria-errormessage'] = accessibilityErrorMessage } if (accessibilityExpanded != null) { domProps['aria-expanded'] = accessibilityExpanded } if (accessibilityFlowTo != null) { domProps['aria-flowto'] = processIDRefList(accessibilityFlowTo) } if (accessibilityHasPopup != null) { domProps['aria-haspopup'] = accessibilityHasPopup } if (accessibilityHidden === true) { domProps['aria-hidden'] = accessibilityHidden } if (accessibilityInvalid != null) { domProps['aria-invalid'] = accessibilityInvalid } if (accessibilityKeyShortcuts != null && Array.isArray(accessibilityKeyShortcuts)) { domProps['aria-keyshortcuts'] = accessibilityKeyShortcuts.join(' ') } if (accessibilityLabel != null) { domProps['aria-label'] = accessibilityLabel } if (accessibilityLabelledBy != null) { domProps['aria-labelledby'] = processIDRefList(accessibilityLabelledBy) } if (accessibilityLevel != null) { domProps['aria-level'] = accessibilityLevel } if (accessibilityLiveRegion != null) { domProps['aria-live'] = accessibilityLiveRegion === 'none' ? 'off' : accessibilityLiveRegion } if (accessibilityModal != null) { domProps['aria-modal'] = accessibilityModal } if (accessibilityMultiline != null) { domProps['aria-multiline'] = accessibilityMultiline } if (accessibilityMultiSelectable != null) { domProps['aria-multiselectable'] = accessibilityMultiSelectable } if (accessibilityOrientation != null) { domProps['aria-orientation'] = accessibilityOrientation } if (accessibilityOwns != null) { domProps['aria-owns'] = processIDRefList(accessibilityOwns) } if (accessibilityPlaceholder != null) { domProps['aria-placeholder'] = accessibilityPlaceholder } if (accessibilityPosInSet != null) { domProps['aria-posinset'] = accessibilityPosInSet } if (accessibilityPressed != null) { domProps['aria-pressed'] = accessibilityPressed } if (accessibilityReadOnly != null) { domProps['aria-readonly'] = accessibilityReadOnly // Enhance with native semantics if ( elementType === 'input' || elementType === 'select' || elementType === 'textarea' ) { domProps.readOnly = true } } if (accessibilityRequired != null) { domProps['aria-required'] = accessibilityRequired // Enhance with native semantics if ( elementType === 'input' || elementType === 'select' || elementType === 'textarea' ) { domProps.required = true } } if (role != null) { // 'presentation' synonym has wider browser support domProps['role'] = role === 'none' ? 'presentation' : role } if (accessibilityRoleDescription != null) { domProps['aria-roledescription'] = accessibilityRoleDescription } if (accessibilityRowCount != null) { domProps['aria-rowcount'] = accessibilityRowCount } if (accessibilityRowIndex != null) { domProps['aria-rowindex'] = accessibilityRowIndex } if (accessibilityRowSpan != null) { domProps['aria-rowspan'] = accessibilityRowSpan } if (accessibilitySelected != null) { domProps['aria-selected'] = accessibilitySelected } if (accessibilitySetSize != null) { domProps['aria-setsize'] = accessibilitySetSize } if (accessibilitySort != null) { domProps['aria-sort'] = accessibilitySort } if (accessibilityValueMax != null) { domProps['aria-valuemax'] = accessibilityValueMax } if (accessibilityValueMin != null) { domProps['aria-valuemin'] = accessibilityValueMin } if (accessibilityValueNow != null) { domProps['aria-valuenow'] = accessibilityValueNow } if (accessibilityValueText != null) { domProps['aria-valuetext'] = accessibilityValueText } // "dataSet" replaced with "data-*" const tmgCN = dataSet ? dataSet.className : undefined const tmgID = dataSet ? dataSet.id : undefined if (dataSet != null) { for (const dataProp in dataSet) { if (dataProp === 'className' || dataProp === 'id') continue if (hasOwnProperty.call(dataSet, dataProp)) { const dataName = hyphenateString(dataProp) const dataValue = dataSet[dataProp] if (dataValue != null) { domProps[`data-${dataName}`] = dataValue } } } } // FOCUS // "focusable" indicates that an element may be a keyboard tab-stop. if (focusable === false) { domProps.tabIndex = '-1' } if ( // These native elements are keyboard focusable by default elementType === 'a' || elementType === 'button' || elementType === 'input' || elementType === 'select' || elementType === 'textarea' ) { if (focusable === false || accessibilityDisabled === true) { domProps.tabIndex = '-1' } } else if ( // These roles are made keyboard focusable by default role === 'button' || role === 'checkbox' || role === 'link' || role === 'radio' || role === 'textbox' || role === 'switch' ) { if (focusable !== false) { domProps.tabIndex = '0' } } else { // Everything else must explicitly set the prop if (focusable === true) { domProps.tabIndex = '0' } } // Resolve styles const flat = flattenStyle(style) let className = tmgCN || '' if (props.className) { className += ` ${props.className}` } const stylesAtomic = flat ? getCSSStylesAtomic(flat) : [] stylesFromProps.set(domProps, stylesAtomic) domProps.style = stylesAtomic.reduce((acc, [key, value]) => { if (key[0] === '_' || key.startsWith('is_') || key.startsWith('font_')) { className += ` ${key}` return acc } if (key === '$$css' || key === '') { return acc } acc[key] = value return acc }, {}) if (className) { domProps.className = className } // OTHER // Native element ID const _id = tmgID || id || nativeID if (_id) { domProps.id = _id } // Automated test IDs if (testID != null) { domProps['data-testid'] = testID } return domProps }