diff --git a/src/Theme/Theme.stories.tsx b/src/Theme/Theme.stories.tsx index 0726e0e8..8ec4b4e4 100644 --- a/src/Theme/Theme.stories.tsx +++ b/src/Theme/Theme.stories.tsx @@ -12,7 +12,9 @@ export default { component: Theme, } as Meta -export const Default: Story = (args) => { +export const Default: Story>> = ( + args +) => { const { theme, setTheme } = useTheme() return ( @@ -43,7 +45,24 @@ export const Default: Story = (args) => { } Default.args = {} -export const NestedThemes: Story = (args) => { +export const AsCustomTag: Story< + ThemeProps> +> = (args) => { + const { theme, setTheme } = useTheme() + + return ( + +
+ +
+
+ ) +} +AsCustomTag.args = {} + +export const NestedThemes: Story< + ThemeProps> +> = (args) => { const { theme, setTheme } = useTheme() const renderNestedThemes = (themes: readonly string[]) => { diff --git a/src/Theme/Theme.tsx b/src/Theme/Theme.tsx index 6043c808..980c6318 100644 --- a/src/Theme/Theme.tsx +++ b/src/Theme/Theme.tsx @@ -1,27 +1,90 @@ -import React, { MutableRefObject, useEffect, useRef, useState } from 'react' +import React, { + MutableRefObject, + ElementType, + useEffect, + useRef, + useState, + ComponentPropsWithoutRef, +} from 'react' import { defaultTheme } from '../constants' import { DataTheme, IComponentBaseProps } from '../types' import { ThemeContext } from './ThemeContext' import { getThemeFromClosestAncestor } from './utils' -export type ThemeProps = Omit< - React.HTMLAttributes, - 'onChange' -> & - IComponentBaseProps & { - onChange?: (theme: DataTheme) => void - } +// Polymorphic component with forwardable refs types from: https://www.benmvp.com/blog/forwarding-refs-polymorphic-react-component-typescript/ + +// Source: https://github.com/emotion-js/emotion/blob/master/packages/styled-base/types/helper.d.ts +// A more precise version of just React.ComponentPropsWithoutRef on its own +export type PropsOf< + C extends keyof JSX.IntrinsicElements | React.JSXElementConstructor +> = JSX.LibraryManagedAttributes> + +type AsProp = { + /** + * An override of the default HTML tag. + * Can also be another React component. + */ + as?: C +} + +/** + * Allows for extending a set of props (`ExtendedProps`) by an overriding set of props + * (`OverrideProps`), ensuring that any duplicates are overridden by the overriding + * set of props. + */ +type ExtendableProps = OverrideProps & + Omit + +/** + * Allows for inheriting the props from the specified element type so that + * props like children, className & style work, as well as element-specific + * attributes like aria roles. The component (`C`) must be passed in. + */ +type InheritableElementProps< + C extends React.ElementType, + Props = {} +> = ExtendableProps, Props> + +/** + * A more sophisticated version of `InheritableElementProps` where + * the passed in `as` prop will determine which props can be included + */ +type PolymorphicComponentProps< + C extends React.ElementType, + Props = {} +> = InheritableElementProps> + +/** * Utility type to extract the `ref` prop from a polymorphic component */ +type PolymorphicRef = + React.ComponentPropsWithRef['ref'] +/** * A wrapper of `PolymorphicComponentProps` that also includes the `ref` * prop for the polymorphic component */ +type PolymorphicComponentPropsWithRef< + C extends React.ElementType, + Props = {} +> = PolymorphicComponentProps & { ref?: PolymorphicRef } + +interface Props { + children?: React.ReactNode + onChange?: (theme: DataTheme) => void +} + +export type ThemeProps = + PolymorphicComponentPropsWithRef & IComponentBaseProps + +type ThemeComponent = ( + props: ThemeProps +) => React.ReactElement | null + +const Theme: ThemeComponent = React.forwardRef( + ( + { as, children, dataTheme, onChange, ...props }: ThemeProps, + ref?: PolymorphicRef + ) => { + const Component = as || 'div' -const Theme = React.forwardRef( - ( - { children, dataTheme, onChange, className, ...props }, - ref - ): JSX.Element => { // Either use provided ref or create a new ref - const themeRef = useRef( - (ref as MutableRefObject)?.current - ) + const themeRef = useRef>(ref?.current) const closestAncestorTheme = getThemeFromClosestAncestor(themeRef) @@ -46,11 +109,12 @@ const Theme = React.forwardRef( return ( -
+ {children} -
+
) } ) + export default Theme diff --git a/src/Theme/index.tsx b/src/Theme/index.tsx index 78540faf..aabd5103 100644 --- a/src/Theme/index.tsx +++ b/src/Theme/index.tsx @@ -1,3 +1,3 @@ import Theme, { ThemeProps as TThemeProps } from './Theme' -export type ThemeProps = TThemeProps +export type ThemeProps = TThemeProps export default Theme diff --git a/src/index.ts b/src/index.ts index 512f208c..f05bb020 100644 --- a/src/index.ts +++ b/src/index.ts @@ -245,5 +245,5 @@ export type WindowMockupProps = TWindowMockupProps // Utils > Theme export { default as Theme } from './Theme' import { ThemeProps as TThemeProps } from './Theme' -export type ThemeProps = TThemeProps +export type ThemeProps = TThemeProps export { useTheme } from './Theme/useTheme'