Skip to content

Commit

Permalink
refactor: polymorphic element type in common props (#420)
Browse files Browse the repository at this point in the history
  • Loading branch information
jer3m01 authored May 29, 2024
1 parent 025db21 commit b0804a4
Show file tree
Hide file tree
Showing 149 changed files with 1,373 additions and 989 deletions.
34 changes: 19 additions & 15 deletions apps/docs/src/routes/docs/core/overview/polymorphism.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ to your end users.
Every component that renders an HTML element has the following types:

- `ComponentOptions`
- `ComponentCommonProps`
- `ComponentCommonProps<T>`
- `ComponentRenderProps`
- `ComponentProps`
- `ComponentProps<T>`

For example, `Tabs.Trigger` has the types `TabsTriggerOptions`, `TabsTriggerCommonProps`,
`TabsTriggerRenderProps` and `TabsTriggerProps`.
For example, `Tabs.Trigger` has the types `TabsTriggerOptions`, `TabsTriggerCommonProps<T>`,
`TabsTriggerRenderProps` and `TabsTriggerProps<T>`.

Components themselves accept props as `PolymorphicProps<T, ComponentProps>` where `T` is a generic
that extends `ValidComponent` and `ComponentProps` are the props of the Kobalte component.
Expand All @@ -123,22 +123,23 @@ This type allows components to accept Kobalte's props and all other props accept
This type contains all custom props consumed by Kobalte, these props do not exist in HTML.
These are not passed to the HTML element nor to the `as` callback.

### `ComponentCommonProps`
### `ComponentCommonProps<T>`

This type contains HTML attributes optionally accepted by the Kobalte component and will
be forwarded to the rendered DOM node. These are managed by Kobalte but can be customized by the end
user. It includes attributes such as `id`, `ref`, event handlers, etc.
user. It includes attributes such as `id`, `ref`, event handlers, etc. The generic is used by `ref` and event handlers,
by default it is `HTMLElement`.

### `ComponentRenderProps`

This type extends `ComponentCommonProps` and additionally contains attributes that are passed
to the DOM node and fully managed by Kobalte. You should never assign these yourself or set them on
the Kobalte component. Modifying these props will break your component's behavior and accessibity.

### `ComponentProps`
### `ComponentProps<T>`

This is the final type exported by components, it is equal to `ComponentOptions & Partial<ComponentCommonProps>`.
It combines all props expected by Kobalte's component.
It combines all props expected by Kobalte's component. The generic is used by the CommonProps, by default it is `HTMLElement`.

### `PolymorphicProps<T, ComponentProps>`

Expand All @@ -151,13 +152,13 @@ import { PolymorphicProps } from "@kobalte/core/polymorphic";

// Optionally extend `TabsTriggerProps` if you wish to
// expose Kobalte props to your end user.
interface CustomProps extends TabsTriggerProps {
interface CustomProps<T extends ValidComponent = "button"> extends TabsTriggerProps<T> {
variant: "default" | "outline";
}

// Your generic `T` should extend ValidComponent and have a default value of the default DOM node.
function CustomTabsTrigger<T extends ValidComponent = "button">(
props: PolymorphicProps<T, CustomProps>,
props: PolymorphicProps<T, CustomProps<T>>,
) {
// Typescript degrades typechecking when using generics, as long as we
// spread `others` to our element, we can effectively ignore them.
Expand All @@ -178,7 +179,7 @@ function CustomTabsTrigger<T extends ValidComponent = "button">(

If you do not wish to allow changing the element type, you can simplify your types by making
props: `OverrideComponentProps<"button", CustomProps>`, replace `"button"` with the correct
tagname for other components.
tagname for other components, imported from `"@kobalte/utils"`.

If you also want to export exact types, you can re-export and extends component types:

Expand All @@ -187,7 +188,7 @@ export interface CustomTabsTriggerOptions extends TabsTriggerOptions {
variant: "default" | "outline";
}

export interface CustomTabsTriggerCommonProps extends TabsTriggerCommonProps {
export interface CustomTabsTriggerCommonProps<T extends HTMLElement = HTMLElement> extends TabsTriggerCommonProps<T> {
// If you allow users to set classes and extend them.
//class: string;
}
Expand All @@ -199,10 +200,13 @@ export interface CustomTabsTriggerRenderProps
class: string;
}

export type CustomTabsTriggerProps = CustomTabsTriggerOptions &
Partial<CustomTabsTriggerCommonProps>;
export type CustomTabsTriggerProps<T extends ValidComponent = "button"> = CustomTabsTriggerOptions &
Partial<CustomTabsTriggerCommonProps<ElementOf<T>>>;

export function CustomTabsTrigger<T extends ValidComponent = "button">(
props: PolymorphicProps<T, CustomTabsTriggerProps>,
props: PolymorphicProps<T, CustomTabsTriggerProps<T>,
) {}
```
`ElementOf<T>` is a helper from `"@kobalte/core/polymorphic"` that converts a tag name into its element
(e.g. `ElementOf<"button"> = HTMLButtonElement`).
15 changes: 9 additions & 6 deletions packages/core/src/accordion/accordion-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import {
} from "solid-js";

import * as Collapsible from "../collapsible";
import { PolymorphicProps } from "../polymorphic";
import { ElementOf, PolymorphicProps } from "../polymorphic";
import { useAccordionItemContext } from "./accordion-item-context";

export interface AccordionContentOptions {}

export interface AccordionContentCommonProps
extends Collapsible.CollapsibleContentCommonProps {
export interface AccordionContentCommonProps<
T extends HTMLElement = HTMLElement,
> extends Collapsible.CollapsibleContentCommonProps<T> {
id: string;
}

Expand All @@ -33,14 +34,16 @@ export interface AccordionContentRenderProps
"aria-labelledby": string | undefined;
}

export type AccordionContentProps = AccordionContentOptions &
Partial<AccordionContentCommonProps>;
export type AccordionContentProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = AccordionContentOptions &
Partial<AccordionContentCommonProps<ElementOf<T>>>;

/**
* Contains the content to be rendered when the `Accordion.Item` is expanded.
*/
export function AccordionContent<T extends ValidComponent = "div">(
props: PolymorphicProps<T, AccordionContentProps>,
props: PolymorphicProps<T, AccordionContentProps<T>>,
) {
const itemContext = useAccordionItemContext();

Expand Down
13 changes: 8 additions & 5 deletions packages/core/src/accordion/accordion-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,28 @@ import {
CollapsibleDataSet,
useCollapsibleContext,
} from "../collapsible/collapsible-context";
import { Polymorphic, PolymorphicProps } from "../polymorphic";
import { ElementOf, Polymorphic, PolymorphicProps } from "../polymorphic";

export interface AccordionHeaderOptions {}

export interface AccordionHeaderCommonProps {}
export interface AccordionHeaderCommonProps<
T extends HTMLElement = HTMLElement,
> {}

export interface AccordionHeaderRenderProps
extends AccordionHeaderCommonProps,
CollapsibleDataSet {}

export type AccordionHeaderProps = AccordionHeaderOptions &
Partial<AccordionHeaderCommonProps>;
export type AccordionHeaderProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = AccordionHeaderOptions & Partial<AccordionHeaderCommonProps<ElementOf<T>>>;

/**
* Wraps an `Accordion.Trigger`.
* Use the `as` prop to update it to the appropriate heading level for your page.
*/
export function AccordionHeader<T extends ValidComponent = "h3">(
props: PolymorphicProps<T, AccordionHeaderProps>,
props: PolymorphicProps<T, AccordionHeaderProps<T>>,
) {
// `Accordion.Item` is a `Collapsible.Root`.
const context = useCollapsibleContext();
Expand Down
13 changes: 7 additions & 6 deletions packages/core/src/accordion/accordion-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from "solid-js";

import * as Collapsible from "../collapsible";
import { PolymorphicProps } from "../polymorphic";
import { ElementOf, PolymorphicProps } from "../polymorphic";
import { createRegisterId } from "../primitives";
import { useAccordionContext } from "./accordion-context";
import {
Expand All @@ -39,21 +39,22 @@ export interface AccordionItemOptions {
forceMount?: boolean;
}

export interface AccordionItemCommonProps
extends Collapsible.CollapsibleRootCommonProps {}
export interface AccordionItemCommonProps<T extends HTMLElement = HTMLElement>
extends Collapsible.CollapsibleRootCommonProps<T> {}

export interface AccordionItemRenderProps
extends AccordionItemCommonProps,
Collapsible.CollapsibleRootRenderProps {}

export type AccordionItemProps = AccordionItemOptions &
Partial<AccordionItemRenderProps>;
export type AccordionItemProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = AccordionItemOptions & Partial<AccordionItemRenderProps>;

/**
* An item of the accordion, contains all the parts of a collapsible section.
*/
export function AccordionItem<T extends ValidComponent = "div">(
props: PolymorphicProps<T, AccordionItemProps>,
props: PolymorphicProps<T, AccordionItemProps<T>>,
) {
const accordionContext = useAccordionContext();

Expand Down
21 changes: 11 additions & 10 deletions packages/core/src/accordion/accordion-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from "solid-js";

import { createListState, createSelectableList } from "../list";
import { Polymorphic, PolymorphicProps } from "../polymorphic";
import { ElementOf, Polymorphic, PolymorphicProps } from "../polymorphic";
import { CollectionItemWithRef } from "../primitives";
import { createDomCollection } from "../primitives/create-dom-collection";
import { AccordionContext, AccordionContextValue } from "./accordion-context";
Expand Down Expand Up @@ -49,25 +49,26 @@ export interface AccordionRootOptions {
shouldFocusWrap?: boolean;
}

export interface AccordionRootCommonProps {
export interface AccordionRootCommonProps<T extends HTMLElement = HTMLElement> {
id: string;
ref: HTMLElement | ((el: HTMLElement) => void);
onKeyDown: JSX.EventHandlerUnion<HTMLElement, KeyboardEvent>;
onMouseDown: JSX.EventHandlerUnion<HTMLElement, MouseEvent>;
onFocusIn: JSX.EventHandlerUnion<HTMLElement, FocusEvent>;
onFocusOut: JSX.EventHandlerUnion<HTMLElement, FocusEvent>;
ref: T | ((el: T) => void);
onKeyDown: JSX.EventHandlerUnion<T, KeyboardEvent>;
onMouseDown: JSX.EventHandlerUnion<T, MouseEvent>;
onFocusIn: JSX.EventHandlerUnion<T, FocusEvent>;
onFocusOut: JSX.EventHandlerUnion<T, FocusEvent>;
}

export interface AccordionRootRenderProps extends AccordionRootCommonProps {}

export type AccordionRootProps = AccordionRootOptions &
Partial<AccordionRootCommonProps>;
export type AccordionRootProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = AccordionRootOptions & Partial<AccordionRootCommonProps<ElementOf<T>>>;

/**
* A vertically stacked set of interactive headings that each reveal an associated section of content.
*/
export function AccordionRoot<T extends ValidComponent = "div">(
props: PolymorphicProps<T, AccordionRootProps>,
props: PolymorphicProps<T, AccordionRootProps<T>>,
) {
let ref: HTMLElement | undefined;

Expand Down
25 changes: 14 additions & 11 deletions packages/core/src/accordion/accordion-trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {

import * as Collapsible from "../collapsible";
import { useCollapsibleContext } from "../collapsible/collapsible-context";
import { PolymorphicProps } from "../polymorphic";
import { ElementOf, PolymorphicProps } from "../polymorphic";
import { CollectionItemWithRef } from "../primitives";
import { createDomCollectionItem } from "../primitives/create-dom-collection";
import { createSelectableItem } from "../selection";
Expand All @@ -32,14 +32,15 @@ import { useAccordionItemContext } from "./accordion-item-context";

export interface AccordionTriggerOptions {}

export interface AccordionTriggerCommonProps
extends Collapsible.CollapsibleTriggerCommonProps {
export interface AccordionTriggerCommonProps<
T extends HTMLElement = HTMLElement,
> extends Collapsible.CollapsibleTriggerCommonProps<T> {
id: string;
onPointerDown: JSX.EventHandlerUnion<HTMLElement, PointerEvent>;
onPointerUp: JSX.EventHandlerUnion<HTMLElement, PointerEvent>;
onKeyDown: JSX.EventHandlerUnion<HTMLElement, KeyboardEvent>;
onMouseDown: JSX.EventHandlerUnion<HTMLElement, MouseEvent>;
onFocus: JSX.EventHandlerUnion<HTMLElement, FocusEvent>;
onPointerDown: JSX.EventHandlerUnion<T, PointerEvent>;
onPointerUp: JSX.EventHandlerUnion<T, PointerEvent>;
onKeyDown: JSX.EventHandlerUnion<T, KeyboardEvent>;
onMouseDown: JSX.EventHandlerUnion<T, MouseEvent>;
onFocus: JSX.EventHandlerUnion<T, FocusEvent>;
}

export interface AccordionTriggerRenderProps
Expand All @@ -48,14 +49,16 @@ export interface AccordionTriggerRenderProps
"data-key": string | undefined;
}

export type AccordionTriggerProps = AccordionTriggerOptions &
Partial<AccordionTriggerCommonProps>;
export type AccordionTriggerProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = AccordionTriggerOptions &
Partial<AccordionTriggerCommonProps<ElementOf<T>>>;

/**
* Toggles the collapsed state of its associated item. It should be nested inside an `Accordion.Header`.
*/
export function AccordionTrigger<T extends ValidComponent = "button">(
props: PolymorphicProps<T, AccordionTriggerProps>,
props: PolymorphicProps<T, AccordionTriggerProps<T>>,
) {
let ref: HTMLElement | undefined;

Expand Down
15 changes: 9 additions & 6 deletions packages/core/src/alert-dialog/alert-dialog-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,30 @@ import {
DialogContentOptions,
DialogContentRenderProps,
} from "../dialog/dialog-content";
import { PolymorphicProps } from "../polymorphic";
import { ElementOf, PolymorphicProps } from "../polymorphic";

export interface AlertDialogContentOptions extends DialogContentOptions {}

export interface AlertDialogContentCommonProps
extends DialogContentCommonProps {}
export interface AlertDialogContentCommonProps<
T extends HTMLElement = HTMLElement,
> extends DialogContentCommonProps<T> {}

export interface AlertDialogContentRenderProps
extends AlertDialogContentCommonProps,
DialogContentRenderProps {
role: "alertdialog";
}

export type AlertDialogContentProps = AlertDialogContentOptions &
Partial<AlertDialogContentCommonProps>;
export type AlertDialogContentProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = AlertDialogContentOptions &
Partial<AlertDialogContentCommonProps<ElementOf<T>>>;

/**
* Overrides the regular `Dialog.Content` with role="alertdialog" to interrupt the user.
*/
export function AlertDialogContent<T extends ValidComponent = "div">(
props: PolymorphicProps<T, AlertDialogContentProps>,
props: PolymorphicProps<T, AlertDialogContentProps<T>>,
) {
return (
<DialogContent<
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/alert/alert-root.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { ValidComponent } from "solid-js";

import { Polymorphic, PolymorphicProps } from "../polymorphic";
import { ElementOf, Polymorphic, PolymorphicProps } from "../polymorphic";

export interface AlertRootOptions {}

export interface AlertRootCommonProps {}
export interface AlertRootCommonProps<T extends HTMLElement = HTMLElement> {}

export interface AlertRootRenderProps extends AlertRootCommonProps {
role: "alert";
}

export type AlertRootProps = AlertRootOptions & Partial<AlertRootCommonProps>;
export type AlertRootProps<
T extends ValidComponent | HTMLElement = HTMLElement,
> = AlertRootOptions & Partial<AlertRootCommonProps<ElementOf<T>>>;

/**
* Alert displays a brief, important message
* in a way that attracts the user's attention without interrupting the user's task.
*/
export function AlertRoot<T extends ValidComponent = "div">(
props: PolymorphicProps<T, AlertRootProps>,
props: PolymorphicProps<T, AlertRootProps<T>>,
) {
return (
<Polymorphic<AlertRootRenderProps>
Expand Down
Loading

0 comments on commit b0804a4

Please sign in to comment.