import * as React from 'react'; import { shouldTriggerClickOnEnterOrSpace } from './accessibility'; import { isHTMLElement } from './dom'; export type ImgOnlyAttributes = { [index in Exclude< keyof React.ImgHTMLAttributes, keyof React.HTMLAttributes >]: React.ImgHTMLAttributes[index]; }; export function setRef(element: T, ref?: React.Ref): void { if (ref) { if (typeof ref === 'function') { ref(element); } else { (ref as React.RefObject).current = element; } } } export function multiRef(...refs: Array | undefined>): React.RefObject { let current: T | null = null; return { get current() { return current; }, set current(element) { current = element; refs.forEach((ref) => ref && setRef(element, ref)); }, }; } export const stopPropagation = (event: T): void => event.stopPropagation(); export const preventDefault = (event: T): void => event.preventDefault(); export function addClassNameToElement(element: HTMLElement, className: string): void { const elementClassName = element.getAttribute('class') || ''; const updatedClassName = `${elementClassName}${elementClassName ? ' ' : ''}${className}`; element.setAttribute('class', updatedClassName); } export function removeClassNameFromElement(element: HTMLElement, classNameToRemove: string): void { const classNamesArray = (element.getAttribute('class') || '').split(/\s+/); const elementIndexToRemove = classNamesArray.findIndex( (className) => className === classNameToRemove, ); if (elementIndexToRemove === -1) { return; } classNamesArray.splice(elementIndexToRemove, 1); element.setAttribute('class', classNamesArray.join(' ')); } type ExcludeKeysWithUndefined = { [P in keyof T]?: Exclude; }; export const excludeKeysWithUndefined = >( obj: T, ): ExcludeKeysWithUndefined => { const filteredObj: ExcludeKeysWithUndefined = {}; for (const key in obj) { if (obj.hasOwnProperty(key) && obj[key] !== undefined) { filteredObj[key] = obj[key]; } } return filteredObj; }; export const isDOMTypeElement = < P extends React.HTMLAttributes | React.SVGAttributes, T extends Element, >( element: React.ReactElement, ): element is Omit, 'ref'> & { ref?: React.Ref | undefined } => typeof element.type === 'string'; export function isValidNotReactFragmentElement( children: Parameters[0], ): children is React.ReactElement> { return ( React.isValidElement(children) && // @ts-expect-error: TS2339 $$typeof всегда symbol, в отличии от type, благодаря этому пропускаем лишние проверки на тип. children.$$typeof !== Symbol.for('react.fragment') ); } export function isForwardRefElement< P extends React.HTMLAttributes | React.SVGAttributes, T extends Element, >( children: Parameters[0], ): children is Omit, 'ref'> & { ref?: React.Ref | undefined } { if (!React.isValidElement(children)) { return false; } // @ts-expect-error: TS2339 $$typeof всегда symbol, в отличии от type, благодаря этому пропускаем лишние проверки на тип. // черпаем вдохновение из react-is https://github.com/facebook/react/blob/d48dbb824985166ecb7b2959db03090a8593dce0/packages/react-is/src/ReactIs.js#L119-L121 const typeOfOfType = children.type && children.type.$$typeof; return typeOfOfType === Symbol.for('react.forward_ref'); } /** * При использовании пропа fetchPriority генерируется warning "Invalid DOM property" (версия React 18.*) * Ворнинга нет в React версии 19.*, поэтому пока поддерживаем 2 версии наименования */ export function getFetchPriorityProp(value: React.ImgHTMLAttributes['fetchPriority']): | { fetchPriority: 'high' | 'low' | 'auto' | undefined; } | { fetchpriority: 'high' | 'low' | 'auto' | undefined; } { if (React.version.startsWith('19')) { return { fetchPriority: value }; } return { fetchpriority: value }; } /* * [a11y] * Обрабатывает событие onkeydown * для кастомных доступных элементов: * - role="link" (активация по Enter) * - role="button" (активация по Space и Enter) */ export function clickByKeyboardHandler(event: React.KeyboardEvent): void { if (!isHTMLElement(event.target) || !shouldTriggerClickOnEnterOrSpace(event)) { return; } event.preventDefault(); event.target.click?.(); }