Skip to content

Commit

Permalink
Building Strongly Typed Polymorphic Components (#18274)
Browse files Browse the repository at this point in the history
* feat: adding reusable polymorphicProps

* feat: added PolymorphicProps to a new file

* fix: fixed commnets

* fix: fixed type error by ommiting ref
  • Loading branch information
guidari authored Jan 15, 2025
1 parent 1896aed commit d1c3284
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 49 deletions.
2 changes: 1 addition & 1 deletion packages/react/src/components/ListItem/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import classnames from 'classnames';
import { usePrefix } from '../../internal/usePrefix';
import { Text } from '../Text';

type ListItemProps = ComponentProps<'li'>;
type ListItemProps = Omit<ComponentProps<'li'>, 'ref'>;

export default function ListItem({
className,
Expand Down
104 changes: 56 additions & 48 deletions packages/react/src/components/Text/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,66 +6,74 @@
*/

import PropTypes from 'prop-types';
import React, { ReactNode, useContext, useRef } from 'react';
import { PolymorphicProps } from '../../types/common';
import React, { ReactNode, useContext } from 'react';
import { TextDir } from './TextDirection';
import { TextDirectionContext } from './TextDirectionContext';
import { useMergeRefs } from '@floating-ui/react';
import {
PolymorphicComponentPropWithRef,
PolymorphicRef,
} from '../../internal/PolymorphicProps';

export interface TextBaseProps {
dir?: TextDir | undefined;
children?: ReactNode;
}

export type TextProps<T extends React.ElementType> = PolymorphicProps<
T,
TextBaseProps
>;
const Text = React.forwardRef(function Text<T extends React.ElementType>(
{ as, children, dir = 'auto', ...rest }: TextProps<T>,
ref: React.Ref<HTMLElement>
) {
// TODO: Update with context typing once its been converted to TS
const context = useContext<any>(TextDirectionContext);
const textProps: { dir?: TextDir } = {};
const BaseComponent = as ?? 'span';
const value = {
...context,
};

if (!context) {
textProps.dir = dir;
value.direction = dir;
} else {
const { direction: parentDirection, getTextDirection } = context;

if (getTextDirection && getTextDirection.current) {
const text = getTextFromChildren(children);
const override = getTextDirection.current(text);

if (parentDirection !== override) {
textProps.dir = override;
value.direction = override;
} else if (parentDirection === 'auto') {
textProps.dir = override;
}
} else if (parentDirection !== dir) {
export type TextProps<T extends React.ElementType> =
PolymorphicComponentPropWithRef<T, TextBaseProps>;

type TextComponent = <T extends React.ElementType = 'span'>(
props: TextProps<T>
) => React.ReactElement | any;

const Text: TextComponent = React.forwardRef(
<T extends React.ElementType = 'span'>(
{ as, children, dir = 'auto', ...rest }: TextProps<T>,
ref?: PolymorphicRef<T>
) => {
// TODO: Update with context typing once its been converted to TS
const context = useContext<any>(TextDirectionContext);
const textProps: { dir?: TextDir } = {};
const BaseComponent = as ?? 'span';
const value = {
...context,
};

if (!context) {
textProps.dir = dir;
value.direction = dir;
} else if (parentDirection === 'auto') {
textProps.dir = dir;
} else {
const { direction: parentDirection, getTextDirection } = context;

if (getTextDirection && getTextDirection.current) {
const text = getTextFromChildren(children);
const override = getTextDirection.current(text);

if (parentDirection !== override) {
textProps.dir = override;
value.direction = override;
} else if (parentDirection === 'auto') {
textProps.dir = override;
}
} else if (parentDirection !== dir) {
textProps.dir = dir;
value.direction = dir;
} else if (parentDirection === 'auto') {
textProps.dir = dir;
}
}
}

return (
<TextDirectionContext.Provider value={value}>
<BaseComponent ref={ref} {...rest} {...textProps}>
{children}
</BaseComponent>
</TextDirectionContext.Provider>
);
});
return (
<TextDirectionContext.Provider value={value}>
<BaseComponent ref={ref} {...rest} {...textProps}>
{children}
</BaseComponent>
</TextDirectionContext.Provider>
);
}
);

Text.propTypes = {
(Text as React.FC).propTypes = {
/**
* Provide a custom element type used to render the outermost node
*/
Expand Down
22 changes: 22 additions & 0 deletions packages/react/src/internal/PolymorphicProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type AsProp<C extends React.ElementType> = {
as?: C;
};

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

// This can be used if there is NO need for "ref"
export type PolymorphicComponentProp<
C extends React.ElementType,
Props = {},
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

// This is the type for the "ref" only
export type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>['ref'];

// This is a new type utitlity with ref!
export type PolymorphicComponentPropWithRef<
C extends React.ElementType,
Props = {},
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };

0 comments on commit d1c3284

Please sign in to comment.