import type { AttributeRow, TagName } from './types' /** * The attribute table: every non-event prop in the Tamagui DOM contract. * * `tags` is the authority for the unsupported-prop diagnostic, `type` is the * source the prop-interface generator emits, and `nativeProp` records what * actually carries the value on native. A row with `native: 'none'` still * type-checks and works on web; using it in a native build is a diagnostic. * * Props that only make sense on a text-entry control (`autoCapitalize`, * `enterKeyHint`, `inputMode`, `spellCheck`) are scoped to input and textarea * here even though React Strict DOM declares them globally. See * `compatibility.ts`. * * The `aria-*` rows map to the legacy accessibility props on purpose, and it is * worth knowing why before anyone simplifies them. React Native does accept * some `aria-*` props directly and translates them itself, so those mappings * look redundant — but the set differs per host. `View` handles thirteen of * them, `Text` handles seven, and neither handles `aria-modal`, `aria-posinset` * or `aria-setsize`. Since one row has to be right for every backing a tag can * lower to, the mapping that every host accepts unconditionally is the correct * one. Verified against react-native 0.86.2's `View.js` and `Text.js`, not * against its types. `id` is the exception: View, Text and TextInput all take * it, so that row passes it straight through. */ const TEXT_ENTRY: readonly TagName[] = ['input', 'textarea'] const ANCHOR: readonly TagName[] = ['a'] const IMAGE: readonly TagName[] = ['img'] /** roles authors may set, excluding the abstract roles */ export const ARIA_ROLES: readonly string[] = [ 'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'complementary', 'contentinfo', 'definition', 'deletion', 'dialog', 'document', 'emphasis', 'feed', 'figure', 'form', 'grid', 'gridcell', 'group', 'heading', 'img', 'input', 'insertion', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'marquee', 'math', 'menu', 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'meter', 'navigation', 'none', 'note', 'option', 'paragraph', 'progressbar', 'radio', 'radiogroup', 'region', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'separator', 'slider', 'spinbutton', 'status', 'strong', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer', 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem', ] /** autofill hints shared by web and react native */ export const AUTO_COMPLETE_VALUES: readonly string[] = [ 'additional-name', 'address-line1', 'address-line2', 'birthdate-day', 'birthdate-full', 'birthdate-month', 'birthdate-year', 'cc-csc', 'cc-exp', 'cc-exp-day', 'cc-exp-month', 'cc-exp-year', 'cc-number', 'cc-name', 'cc-given-name', 'cc-middle-name', 'cc-family-name', 'cc-type', 'country', 'current-password', 'email', 'family-name', 'gender', 'given-name', 'honorific-prefix', 'honorific-suffix', 'name', 'name-family', 'name-given', 'name-middle', 'name-middle-initial', 'name-prefix', 'name-suffix', 'new-password', 'nickname', 'on', 'one-time-code', 'organization', 'organization-title', 'password', 'password-new', 'postal-address', 'postal-address-country', 'postal-address-extended', 'postal-address-extended-postal-code', 'postal-address-locality', 'postal-address-region', 'postal-code', 'street-address', 'sms-otp', 'tel', 'tel-country-code', 'tel-national', 'tel-device', 'url', 'username', 'username-new', 'off', ] /** every html input type; the ones native cannot render are a native build error */ export const HTML_INPUT_TYPES: readonly string[] = [ 'checkbox', 'color', 'date', 'datetime-local', 'email', 'file', 'hidden', 'month', 'number', 'password', 'radio', 'range', 'search', 'submit', 'tel', 'text', 'time', 'url', 'week', ] /** * Input types react native can render with a text-entry control. * * `hidden` and `submit` were here and should not have been: hidden renders * nothing at all and submit is a button, so neither is a text-entry control and * claiming native support for them would pass a build that cannot render. */ export const NATIVE_INPUT_TYPES: readonly string[] = [ 'email', 'number', 'password', 'search', 'tel', 'text', 'url', ] const ariaState = (name: string, nativeProp: string | null): AttributeRow => ({ group: 'aria', tags: '*', type: 'boolean', nativeProp, web: 'host', native: nativeProp ? 'polyfill' : 'none', }) const ariaRef = (): AttributeRow => ({ group: 'aria', tags: '*', type: 'string', nativeProp: null, web: 'host', native: 'none', }) const ariaNumber = (nativeProp: string | null = null): AttributeRow => ({ group: 'aria', tags: '*', type: 'number', nativeProp, web: 'host', native: nativeProp ? 'polyfill' : 'none', }) const ariaText = (nativeProp: string | null = null): AttributeRow => ({ group: 'aria', tags: '*', type: 'string', nativeProp, web: 'host', native: nativeProp ? 'polyfill' : 'none', }) export const ATTRIBUTES: Readonly> = { // accessibility 'aria-activedescendant': ariaRef(), 'aria-atomic': ariaState('aria-atomic', null), 'aria-autocomplete': { group: 'aria', tags: '*', type: `'none' | 'inline' | 'list' | 'both'`, nativeProp: null, web: 'host', native: 'none', }, 'aria-busy': ariaState('aria-busy', 'accessibilityState.busy'), 'aria-checked': { group: 'aria', tags: '*', type: `boolean | 'mixed'`, nativeProp: 'accessibilityState.checked', web: 'host', native: 'polyfill', }, 'aria-colcount': ariaNumber(), 'aria-colindex': ariaNumber(), 'aria-colindextext': ariaText(), 'aria-colspan': ariaNumber(), 'aria-controls': ariaRef(), 'aria-current': { group: 'aria', tags: '*', type: `boolean | 'page' | 'step' | 'location' | 'date' | 'time'`, nativeProp: null, web: 'host', native: 'none', }, 'aria-describedby': { ...ariaRef(), note: 'react native has no described-by relationship; put the description in aria-label instead', }, 'aria-details': ariaRef(), 'aria-disabled': ariaState('aria-disabled', 'accessibilityState.disabled'), 'aria-errormessage': ariaRef(), 'aria-expanded': ariaState('aria-expanded', 'accessibilityState.expanded'), 'aria-flowto': ariaRef(), 'aria-haspopup': { group: 'aria', tags: '*', type: `'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'`, nativeProp: null, web: 'host', native: 'none', }, 'aria-hidden': { group: 'aria', tags: '*', type: 'boolean', nativeProp: 'accessibilityElementsHidden', web: 'host', native: 'polyfill', note: 'also sets importantForAccessibility on android so descendants are hidden', }, 'aria-invalid': { group: 'aria', tags: '*', type: `boolean | 'grammar' | 'spelling'`, nativeProp: null, web: 'host', native: 'none', }, 'aria-keyshortcuts': ariaText(), 'aria-label': ariaText('accessibilityLabel'), 'aria-labelledby': { group: 'aria', tags: '*', type: 'string', nativeProp: 'accessibilityLabelledBy', web: 'host', native: 'polyfill', }, 'aria-level': ariaNumber(), 'aria-live': { group: 'aria', tags: '*', type: `'off' | 'assertive' | 'polite'`, nativeProp: 'accessibilityLiveRegion', web: 'host', native: 'polyfill', }, 'aria-modal': ariaState('aria-modal', 'accessibilityViewIsModal'), 'aria-multiline': ariaState('aria-multiline', null), 'aria-multiselectable': ariaState('aria-multiselectable', null), 'aria-orientation': { group: 'aria', tags: '*', type: `'horizontal' | 'vertical'`, nativeProp: null, web: 'host', native: 'none', }, 'aria-owns': ariaRef(), 'aria-placeholder': ariaText(), // react-native 0.86.2 has no accessibilityPosInSet — checked View.js and // ViewAccessibility.d.ts, it exists in neither. Mapping to it would have the // prop silently dropped at the host, which is worse than declaring it web-only 'aria-posinset': ariaNumber(), 'aria-pressed': { group: 'aria', tags: '*', type: `boolean | 'mixed'`, nativeProp: null, web: 'host', native: 'none', }, 'aria-readonly': ariaState('aria-readonly', null), 'aria-required': ariaState('aria-required', null), 'aria-roledescription': ariaText(), 'aria-rowcount': ariaNumber(), 'aria-rowindex': ariaNumber(), 'aria-rowindextext': ariaText(), 'aria-rowspan': ariaNumber(), 'aria-selected': ariaState('aria-selected', 'accessibilityState.selected'), // same: no accessibilitySetSize anywhere in react-native 0.86.2 'aria-setsize': ariaNumber(), 'aria-sort': { group: 'aria', tags: '*', type: `'none' | 'ascending' | 'descending' | 'other'`, nativeProp: null, web: 'host', native: 'none', }, 'aria-valuemax': ariaNumber('accessibilityValue.max'), 'aria-valuemin': ariaNumber('accessibilityValue.min'), 'aria-valuenow': ariaNumber('accessibilityValue.now'), 'aria-valuetext': ariaText('accessibilityValue.text'), // global autoFocus: { group: 'global', tags: '*', type: 'boolean', nativeProp: null, web: 'host', native: 'none', note: 'focus a native element through a ref in an effect instead', }, dir: { group: 'global', tags: '*', type: `'auto' | 'ltr' | 'rtl'`, nativeProp: 'writingDirection', web: 'host', native: 'polyfill', }, elementTiming: { group: 'global', tags: '*', type: 'string', nativeProp: null, web: 'host', native: 'none', note: 'element timing is a browser performance api', }, hidden: { group: 'global', tags: '*', type: `true | 'hidden' | 'until-found'`, nativeProp: 'style.display', web: 'host', native: 'polyfill', note: 'native applies display none and has no until-found reveal', }, id: { group: 'global', tags: '*', type: 'string', // react native takes `id` itself on View, Text and TextInput and assigns it // to nativeID, so there is nothing to rename nativeProp: 'id', web: 'host', native: 'polyfill', }, inert: { group: 'global', tags: '*', type: 'boolean', nativeProp: null, web: 'host', native: 'none', }, lang: { group: 'global', tags: '*', type: 'string', nativeProp: null, web: 'host', native: 'none', }, role: { group: 'global', tags: '*', type: 'AriaRole', nativeProp: 'role', web: 'host', native: 'polyfill', }, tabIndex: { group: 'global', tags: '*', type: '0 | -1', nativeProp: 'focusable', web: 'host', native: 'polyfill', }, // react suppressHydrationWarning: { group: 'react', tags: '*', type: 'boolean', nativeProp: null, web: 'host', native: 'none', }, children: { group: 'react', tags: '*', nativeProp: 'children', web: 'host', native: 'host', note: 'the content model in the tag table decides the type: no children on a void tag, text only inside an option', }, // supplied by the styling frontend, never generated as a prop here style: { group: 'frontend', tags: '*', nativeProp: 'style', web: 'host', native: 'host', note: 'each frontend supplies its own style contract: tamagui style props, className, or style() handles', }, 'data-*': { group: 'data', tags: '*', type: 'string | number | boolean', nativeProp: null, web: 'host', native: 'none', note: 'data-testid is the one exception and reaches testID on native', }, 'data-testid': { group: 'data', tags: '*', type: 'string', nativeProp: 'testID', web: 'host', native: 'polyfill', }, // text entry autoCapitalize: { group: 'element', tags: TEXT_ENTRY, type: `'none' | 'sentences' | 'words' | 'characters'`, nativeProp: 'autoCapitalize', web: 'host', native: 'host', }, autoComplete: { group: 'element', tags: ['input', 'select', 'textarea'], type: 'AutoComplete', nativeProp: 'autoComplete', web: 'host', native: 'host', }, enterKeyHint: { group: 'element', tags: TEXT_ENTRY, type: `'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'`, nativeProp: 'enterKeyHint', web: 'host', native: 'host', }, inputMode: { group: 'element', tags: TEXT_ENTRY, type: `'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'`, nativeProp: 'inputMode', web: 'host', native: 'host', }, spellCheck: { group: 'element', tags: TEXT_ENTRY, type: 'boolean', nativeProp: 'spellCheck', web: 'host', native: 'host', note: 'ios only on native', }, // anchor download: { group: 'element', tags: ANCHOR, type: 'string', nativeProp: null, web: 'host', native: 'none', }, href: { group: 'element', tags: ANCHOR, type: 'string', nativeProp: null, web: 'host', native: 'none', note: 'native has no navigation; handle onClick and call your router or Linking', }, referrerPolicy: { group: 'element', tags: ['a', 'img'], type: `'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'`, nativeProp: 'referrerPolicy', web: 'host', native: 'host', note: 'native honours it for image requests only', }, rel: { group: 'element', tags: ANCHOR, type: 'string', nativeProp: null, web: 'host', native: 'none', }, target: { group: 'element', tags: ANCHOR, type: `'_self' | '_blank' | '_parent' | '_top'`, nativeProp: null, web: 'host', native: 'none', }, // button, input, option, optgroup, textarea disabled: { group: 'element', tags: ['button', 'input', 'optgroup', 'option', 'textarea'], type: 'boolean', nativeProp: 'disabled', web: 'host', native: 'polyfill', note: 'native also clears focusable, and editable on a text-entry control', }, type: { group: 'element', tags: ['button', 'input'], perTag: { button: { type: `'button' | 'submit'` }, input: { type: 'InputType', note: 'on native the type selects the keyboard and secure entry rather than reaching one prop, and the types with no text-entry control are a build error', }, }, nativeProp: null, web: 'host', native: 'polyfill', }, // image alt: { group: 'element', tags: IMAGE, type: 'string', nativeProp: 'alt', web: 'host', native: 'host', }, crossOrigin: { group: 'element', tags: IMAGE, type: `'anonymous' | 'use-credentials'`, nativeProp: 'crossOrigin', web: 'host', native: 'host', }, decoding: { group: 'element', tags: IMAGE, type: `'async' | 'auto' | 'sync'`, nativeProp: null, web: 'host', native: 'none', }, draggable: { group: 'element', tags: IMAGE, type: 'boolean', nativeProp: null, web: 'host', native: 'none', }, fetchPriority: { group: 'element', tags: IMAGE, type: `'high' | 'low' | 'auto'`, nativeProp: null, web: 'host', native: 'none', }, height: { group: 'element', tags: IMAGE, type: 'number', nativeProp: 'height', web: 'host', native: 'host', }, loading: { group: 'element', tags: IMAGE, type: `'eager' | 'lazy'`, nativeProp: null, web: 'host', native: 'none', }, src: { group: 'element', tags: IMAGE, type: 'string', nativeProp: 'src', web: 'host', native: 'host', }, srcSet: { group: 'element', tags: IMAGE, type: 'string', nativeProp: 'srcSet', web: 'host', native: 'host', }, width: { group: 'element', tags: IMAGE, type: 'number', nativeProp: 'width', web: 'host', native: 'host', }, // form controls checked: { group: 'element', tags: ['input'], type: `boolean | 'mixed'`, nativeProp: null, web: 'host', native: 'none', note: 'checkbox and radio inputs have no native rendering', }, defaultChecked: { group: 'element', tags: ['input'], type: 'boolean', nativeProp: null, web: 'host', native: 'none', note: 'checkbox and radio inputs have no native rendering', }, defaultValue: { group: 'element', tags: ['input', 'select', 'textarea'], type: 'string', perTag: { select: { type: 'string | readonly string[]' } }, nativeProp: 'defaultValue', web: 'host', native: 'host', }, for: { group: 'element', tags: ['label'], type: 'string', nativeProp: null, web: 'host', native: 'none', note: 'native has no label association; wrap the control or drive it from your own state', }, label: { group: 'element', tags: ['optgroup', 'option'], type: 'string', nativeProp: null, web: 'host', native: 'none', }, max: { group: 'element', tags: ['input'], type: 'string | number', nativeProp: null, web: 'host', native: 'none', }, maxLength: { group: 'element', tags: TEXT_ENTRY, type: 'number', nativeProp: 'maxLength', web: 'host', native: 'host', }, min: { group: 'element', tags: ['input'], type: 'string | number', nativeProp: null, web: 'host', native: 'none', }, minLength: { group: 'element', tags: TEXT_ENTRY, type: 'number', nativeProp: null, web: 'host', native: 'none', }, multiple: { group: 'element', tags: ['input', 'select'], type: 'boolean', nativeProp: null, web: 'host', native: 'none', }, name: { group: 'element', tags: ['input', 'select', 'textarea'], type: 'string', nativeProp: null, web: 'host', native: 'none', note: 'form field names only matter to a form submission, which native does not have', }, placeholder: { group: 'element', tags: TEXT_ENTRY, type: 'string', nativeProp: 'placeholder', web: 'host', native: 'host', }, readOnly: { group: 'element', tags: TEXT_ENTRY, type: 'boolean', nativeProp: 'editable', web: 'host', native: 'polyfill', }, required: { group: 'element', tags: ['input', 'select', 'textarea'], type: 'boolean', nativeProp: null, web: 'host', native: 'none', note: 'native has no form validation', }, rows: { group: 'element', tags: ['textarea'], type: 'number', nativeProp: 'numberOfLines', web: 'host', native: 'polyfill', }, step: { group: 'element', tags: ['input'], type: `number | 'any'`, nativeProp: null, web: 'host', native: 'none', }, value: { group: 'element', tags: ['input', 'li', 'option', 'select', 'textarea'], type: 'string', perTag: { li: { type: 'number', note: 'the list counter is not rendered on either platform, matching the list-style reset', }, select: { type: 'string | readonly string[]' }, }, nativeProp: 'value', web: 'host', native: 'polyfill', }, } export const ATTRIBUTE_NAMES: readonly string[] = Object.keys(ATTRIBUTES) /** props the styling frontend supplies, so the DOM contract does not generate them */ export const FRONTEND_PROPS: readonly string[] = ['style']