Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: new polymorphic implementation #381

Merged
merged 53 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
3d3da8e
refactor: new polymorphic implementation
jer3m01 Apr 2, 2024
55823fe
chore: correct type definitions
jer3m01 Apr 3, 2024
0769c52
refactor(collapsible): polymorphic
jer3m01 Apr 3, 2024
1b91038
refactor(accordion): polymorphic
jer3m01 Apr 3, 2024
35d3029
style: format
jer3m01 Apr 3, 2024
e628a98
refactor(button): polymorphic
jer3m01 Apr 3, 2024
491d00a
refactor(collapsible): extend trigger props
jer3m01 Apr 3, 2024
a0248a4
style: format
jer3m01 Apr 3, 2024
702a849
refactor(alert): polymorphic
jer3m01 Apr 3, 2024
15dd5f5
refactor(link): polymorphic
jer3m01 Apr 3, 2024
03aa3df
chore: remove unused import
jer3m01 Apr 3, 2024
2481390
refactor(breadcrumbs): polymorphic
jer3m01 Apr 3, 2024
2d0ce07
chore: correct type definitions
jer3m01 Apr 3, 2024
2782636
refactor(form-control): polymorphic
jer3m01 Apr 3, 2024
03a1755
refactor(checkbox): polymorphic
jer3m01 Apr 3, 2024
bca2cf0
refactor(dismissable-layer): polymorphic
jer3m01 Apr 3, 2024
4a3549c
refactor(image): polymorphic
jer3m01 Apr 3, 2024
5bb2e85
refactor(skeleton): polymorphic
jer3m01 Apr 3, 2024
361f049
refactor(dialog): polymorphic
jer3m01 Apr 5, 2024
c5f2f65
chore: correct type definitions
jer3m01 Apr 5, 2024
f77a119
refactor(alert-dialog): polymorphic
jer3m01 Apr 6, 2024
774228a
refactor(listbox): polymorphic
jer3m01 Apr 6, 2024
ad95a8f
refactor(select): polymorphic
jer3m01 Apr 7, 2024
942a975
refactor(combobox): polymorphic
jer3m01 Apr 7, 2024
136b67e
refactor(pagination): polymorphic
jer3m01 Apr 19, 2024
d0cf0b0
refactor(separator): polymorphic
jer3m01 Apr 19, 2024
0221258
refactor(spin-button): polymorphic
jer3m01 Apr 19, 2024
0b2006a
refactor(toggle-button): polymorphic
jer3m01 Apr 19, 2024
c4d0d4b
refactor(toggle-group): polymorphic
jer3m01 Apr 19, 2024
98adde5
refactor(radio-group): polymorphic
jer3m01 Apr 20, 2024
a518f0a
refactor(switch): polymorphic
jer3m01 Apr 22, 2024
14a5150
refactor(progress): polymorphic
jer3m01 Apr 22, 2024
d473667
refactor(tabs): polymorphic
jer3m01 Apr 22, 2024
f2f8776
chore: correct type definitions
jer3m01 Apr 22, 2024
da0248d
refactor(number-field): polymorphic
jer3m01 Apr 22, 2024
efc6c30
refactor(text-field): polymorphic
jer3m01 Apr 22, 2024
63ce103
refactor(toast): polymorphic
jer3m01 Apr 24, 2024
ba748fd
refactor(slider): polymorphic
jer3m01 Apr 24, 2024
83f4472
refactor(popper): polymorphic
jer3m01 Apr 24, 2024
e14fe66
refactor(tooltip): polymorphic
jer3m01 Apr 24, 2024
075de11
style: format
jer3m01 Apr 24, 2024
9ee4d3b
refactor(popover): polymorphic
jer3m01 Apr 24, 2024
de63291
refactor(hover-card): polymorphic
jer3m01 Apr 24, 2024
ce854ab
refactor(menu): polymorphic
jer3m01 Apr 27, 2024
6a45b22
chore: correct type definitions
jer3m01 Apr 27, 2024
91fade1
chore: correct type definitions
jer3m01 Apr 27, 2024
0a2e468
refactor(context-menu): polymorphic
jer3m01 Apr 27, 2024
20f6c8b
style: format
jer3m01 Apr 27, 2024
7c25cd5
refactor(dropdown-menu): polymorphic
jer3m01 Apr 27, 2024
7b1b4ca
refactor(menubar): polymorphic
jer3m01 Apr 27, 2024
03d8738
docs: document polymorphic
jer3m01 Apr 27, 2024
cf10891
chore: correct type definitions
jer3m01 Apr 27, 2024
6ac198a
test: fix polymorphic
jer3m01 Apr 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 37 additions & 39 deletions apps/docs/src/examples/select.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { As, Select } from "@kobalte/core";
import { Select } from "@kobalte/core";
import { createVirtualizer } from "@tanstack/solid-virtual";
import { For, createSignal } from "solid-js";

Expand Down Expand Up @@ -377,46 +377,44 @@ export function MultipleSelectionExample() {
<Select.Trigger
class={`${style.select__trigger} ${style.select__trigger_multi}`}
aria-label="Fruits"
asChild
as="div"
>
<As component="div">
<Select.Value<string> class={style.select__value}>
{(state) => (
<>
<div class="flex items-center gap-2 flex-wrap">
<For each={state.selectedOptions()}>
{(option) => (
<span
class="bg-zinc-100 dark:bg-zinc-700 text-sm px-2 py-0.5 rounded inline-flex items-center gap-x-2"
onPointerDown={(e) => e.stopPropagation()}
<Select.Value<string> class={style.select__value}>
{(state) => (
<>
<div class="flex items-center gap-2 flex-wrap">
<For each={state.selectedOptions()}>
{(option) => (
<span
class="bg-zinc-100 dark:bg-zinc-700 text-sm px-2 py-0.5 rounded inline-flex items-center gap-x-2"
onPointerDown={(e) => e.stopPropagation()}
>
{option}
<button
type="button"
onClick={() => state.remove(option)}
class="rounded-full hover:bg-zinc-300 dark:hover:bg-zinc-600 p-1"
>
{option}
<button
type="button"
onClick={() => state.remove(option)}
class="rounded-full hover:bg-zinc-300 dark:hover:bg-zinc-600 p-1"
>
<CrossIcon class="h3 w-3" />
</button>
</span>
)}
</For>
</div>
<button
type="button"
onPointerDown={(e) => e.stopPropagation()}
onClick={state.clear}
class="ml-auto mr-2 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-600 p-1"
>
<CrossIcon class="h-3.5 w-3.5" />
</button>
</>
)}
</Select.Value>
<Select.Icon class={style.select__icon}>
<CaretSortIcon />
</Select.Icon>
</As>
<CrossIcon class="h3 w-3" />
</button>
</span>
)}
</For>
</div>
<button
type="button"
onPointerDown={(e) => e.stopPropagation()}
onClick={state.clear}
class="ml-auto mr-2 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-600 p-1"
>
<CrossIcon class="h-3.5 w-3.5" />
</button>
</>
)}
</Select.Value>
<Select.Icon class={style.select__icon}>
<CaretSortIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content class={style.select__content}>
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/routes/docs/core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ const CORE_NAV_SECTIONS: NavSection[] = [
{
title: "Polymorphism",
href: "/docs/core/overview/polymorphism",
status: "updated",
},
{
title: "Server side rendering",
href: "/docs/core/overview/ssr",
status: "updated",
},
],
},
Expand Down
54 changes: 26 additions & 28 deletions apps/docs/src/routes/docs/core/components/select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ The `multiple` prop can be used to create a select that allow multi-selection. I

The `Select.Value` children _render prop_ expose an array of selected options, and two method for removing an option from the selection and clear the selection.

Additionally, the example below uses the `asChild` pattern to render a `div` for the `Select.Trigger` since HTML button can't contain interactive elements according to the [W3C](https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element).
Additionally, the example below uses the `as` prop to render a `div` for the `Select.Trigger` since HTML button can't contain interactive elements according to the [W3C](https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element).

<Preview>
<MultipleSelectionExample />
Expand Down Expand Up @@ -681,33 +681,31 @@ function MultipleSelectionExample() {
</Select.Item>
)}
>
<Select.Trigger aria-label="Fruits" asChild>
<As component="div">
<Select.Value<string>>
{state => (
<>
<div>
<For each={state.selectedOptions()}>
{option => (
<span onPointerDown={e => e.stopPropagation()}>
{option}
<button onClick={() => state.remove(option)}>
<CrossIcon />
</button>
</span>
)}
</For>
</div>
<button onPointerDown={e => e.stopPropagation()} onClick={state.clear}>
<CrossIcon />
</button>
</>
)}
</Select.Value>
<Select.Icon>
<CaretSortIcon />
</Select.Icon>
</As>
<Select.Trigger aria-label="Fruits" as="div">
<Select.Value<string>>
{state => (
<>
<div>
<For each={state.selectedOptions()}>
{option => (
<span onPointerDown={e => e.stopPropagation()}>
{option}
<button onClick={() => state.remove(option)}>
<CrossIcon />
</button>
</span>
)}
</For>
</div>
<button onPointerDown={e => e.stopPropagation()} onClick={state.clear}>
<CrossIcon />
</button>
</>
)}
</Select.Value>
<Select.Icon>
<CaretSortIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content>
Expand Down
161 changes: 147 additions & 14 deletions apps/docs/src/routes/docs/core/overview/polymorphism.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Polymorphism

All component parts that render a DOM element have an `as` and a `asChild` prop.
All component parts that render a DOM element have an `as` prop.

## The `as` prop

Expand Down Expand Up @@ -30,19 +30,20 @@ function App() {
}
```

## The `asChild` prop
## The `as` prop callback

For more advanced use cases the `asChild` prop and the `<As>` component can be used.
The main reason to use `asChild` over the `as` prop is being able to set props without interfering with Kobalte.
For more advanced use cases the `as` prop can accept a callback.
The main reason to use a callback over the normal `as` prop is being able to set props without interfering with Kobalte.

When using this pattern the following rules apply to the component with the `asChild` prop, and it's child `<As>` component:
When using this pattern the following rules apply to the callback:

- CSS classes are combined.
- Props are combined, if same attribute exists the one from `<As>` win.
- Event handlers are chained, the one from `<As>` get called first.
- You must spread the props forwarded to your callback onto your node/component.
- Custom props are passed as is from the parent.
- Kobalte options are not passed to the callback, only the resulting html attributes.
- You should set your event handlers on the parent and not inside your callback.

```tsx {15}
import { As, Tabs as KTabs } from "@kobalte/core";
import { Tabs as KTabs } from "@kobalte/core";
import { MyCustomButton } from "./components";

function App() {
Expand All @@ -55,15 +56,147 @@ function App() {
</KTabs.Trigger>

{/* The `value` prop is used by Kobalte and not passed to MyCustomButton */}
<KTabs.Trigger value="one" asChild>
{/* The `value` prop is directly passed to MyCustomButton */}
<As component={MyCustomButton} value="custom">
Custom Button Trigger
</As>
<KTabs.Trigger
value="one"
as={props => (
// The `value` prop is directly passed to MyCustomButton
<MyCustomButton value="custom" {...props} />
)}
>
Custom Button Trigger
</KTabs.Trigger>
</KTabs.List>
<KTabs.Content value="one">Content one</KTabs.Content>
</KTabs.Root>
);
}
```

You can optionally use a type helper to get the exact types passed to your callback:

```tsx {4}
import { Tabs as KTabs, PolymorphicCallbackProps } from "@kobalte/core";

<KTabs.Trigger
value="one"
as={(
props: PolymorphicCallbackProps<
MyCustomButtonProps,
KTabs.TabsTriggerOptions,
KTabs.TabsTriggerRenderProps
>,
) => (
// The `value` prop is directly passed to MyCustomButton
<MyCustomButton value="custom" {...props} />
)}
>
Custom Button Trigger
</KTabs.Trigger>;
```

## Event lifecycle

Setting custom event handlers on component will call your custom handler before Kobalte's.

## Types

This section is mainly for library author that want to build on top of Kobalte and expose the correct types
to your end users.

Every component that renders an HTML element has the following types:

- `ComponentOptions`
- `ComponentCommonProps`
- `ComponentRenderProps`
- `ComponentProps`

For example, `Tabs.Trigger` has the types `TabsTriggerOptions`, `TabsTriggerCommonProps`,
`TabsTriggerRenderProps` and `TabsTriggerProps` namespaced as `Tabs.TabsTriggerOptions`, etc.

Components themselves accept props as `PolymorphicProps<T, ComponentProps>` where `T` is a generic
that extends `ValidComponent` and the `ComponentProps` of the Kobalte component.
This type allows components to accept Kobalte's props and all other props accepted by `T`.

### `ComponentOptions`

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`

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.

### `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`

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

### `PolymorphicProps<T, ComponentProps>`

If you're writing a custom component and want to expose Kobalte's `as` prop to the end user
and keep proper typing, be sure to use `PolymorphicProps<T, ComponentProps>` for your props type.

```tsx
import { Tabs as KTabs, PolymorphicProps } from "@kobalte/core";

// Optionally extend `KTabs.TabsTriggerProps` if you wish to
// expose Kobalte props to your end user.
interface CustomProps extends KTabs.TabsTriggerProps {
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>,
) {
// Typescript degrades typechecking when using generics, as long as we
// spread `others` to our element, we can effectively ignore them.
const [local, others] = splitProps(props as CustomProps, ["variant"]);

return (
<KTabs.Trigger
// Optional, will default to Kobalte otherwise.
// This should match with your generic `T` default.
as="button"
class={local.variant === "default" ? "default-trigger" : "outline-trigger"}
// Make sure to spread these props!
{...others}
/>
);
}
```

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

```tsx
export interface CustomTabsTriggerOptions extends KTabs.TabsTriggerOptions {
variant: "default" | "outline";
}

export interface CustomTabsTriggerCommonProps extends KTabs.TabsTriggerCommonProps {
// If you allow users to set classes and extend them.
//class: string;
}

export interface CustomTabsTriggerRenderProps
extends CustomTabsTriggerCommonProps,
KTabs.TabsTriggerRenderProps {
// If you do not allow users to set classes and manage all of them.
class: string;
}

export type CustomTabsTriggerProps = CustomTabsTriggerOptions &
Partial<CustomTabsTriggerCommonProps>;

export function CustomTabsTrigger<T extends ValidComponent = "button">(
props: PolymorphicProps<T, CustomTabsTriggerProps>,
) {}
```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"typescript": "4.9.5",
"vite": "5.0.11",
"vite-plugin-solid": "2.9.1",
"vitest": "1.3.1"
"vitest": "1.3.1",
"@vitest/ui": "^1.5.2"
},
"packageManager": "[email protected]"
}
Loading
Loading