Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
117 changes: 69 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,41 @@ Bring [Slots](https://github.com/reactjs/rfcs/pull/223) to React, with SSR suppo

## Usage

### Simple version (only one slot is used per slot type)

1. Create your component with slots

```tsx
import React, { useId } from 'react'
import createSlots from 'create-slots'
import { createHost, createSlot } from 'create-slots'

const { createHost, SlotComponents, useSlots } = createSlots({
Label: 'label',
Input: 'input',
Description: 'div',
})
const FieldLabel = createSlot('label')
const FieldInput = createSlot('input')
const FieldDescription = createSlot('div')

type FieldProps = React.ComponentPropsWithoutRef<'div'>

const FieldBase: React.FC<FieldProps> = (props) => {
const Slots = useSlots()
export const Field = (props: FieldProps) => {
const id = useId()
const inputId = Slots.getProps('Input')?.id || `${id}-label`
const descriptionId = Slots.has('Description') ? `${id}-desc` : undefined

return (
<div {...props}>
{Slots.render('Label', { htmlFor: inputId })}
{Slots.render('Input', {
id: inputId,
'aria-describedby': descriptionId,
})}
{Slots.render('Description', { id: descriptionId })}
</div>
)
return createHost(props.children, (Slots) => {
const labelProps = Slots.getProps(FieldLabel)
const inputProps = Slots.getProps(FieldInput)
const inputId = inputProps?.id || id

return (
<div {...props}>
{labelProps && <label {...labelProps} htmlFor={inputId} />}
<input id={id} {...inputProps} />
{Slots.get('Description')}
</div>
)
})
}

export const Field = Object.assign(createHost(FieldBase), SlotComponents)
Field.Label = FieldLabel
Field.Input = FieldInput
Field.Description = FieldDescription
```

2. Use it
Expand All @@ -57,43 +59,62 @@ export const Field = Object.assign(createHost(FieldBase), SlotComponents)
</Field>
```

### List slots
### List slots (fully implemented the [React Slots RFC](https://github.com/reactjs/rfcs/pull/223) with utils)

```tsx
import React, { useState } from 'react'
import createSlots from 'create-slots/list'

const { createHost, SlotComponents, useSlots } = createSlots({
Item: 'li',
Divider: 'hr',
})

const SelectBase: React.FC<React.ComponentPropsWithoutRef<'ul'>> = (props) => {
const [selected, setSelected] = useState<React.ReactNode>(null)
const slotItems = useSlots().renderItems(
({ name, props: itemProps, index }) => {
if (name === 'Item') {
return {
...itemProps,
'data-index': index,
'aria-selected': itemProps.children === selected,
onClick: () => {
setSelected(itemProps.value)
},
}
}
}
)
import { createHost, createSlot, getSlotProps, isSlot } from 'create-slots/list'

const SelectItem = createSlot('li')
const SelectDivider = createSlot('hr')

type SelectProps = React.FC<React.ComponentPropsWithoutRef<'ul'>>

const Select = (props: SelectProps) => {
const [selected, setSelected] = useState<string>()
const indexRef = React.useRef(0)

return (
<div>
<div>Selected: {selected}</div>
<ul {...props}>{slotItems}</ul>
<div>Selected: {selected ?? ''}</div>
{createHost(props.children, (slots) => {
indexRef.current = 0
return (
<ul {...props}>
{slots.map((slot) => {
if (isSlot(slot, SelectItem)) {
const slotProps = getSlotProps(slot)
return (
<li
{...slotProps}
data-index={indexRef.current++}
aria-selected={slotProps.value === selected}
onClick={() => setSelected(slotProps.value as string)}
/>
)
}

return slot
})}
</ul>
)
})}
</div>
)
}

export const Select = Object.assign(createHost(SelectBase), SlotComponents)
Select.Item = SelectItem
Select.Divider = SelectDivider
```

2. Use it

```tsx
<Select>
<Select.Item value="foo">Foo</Select.Item>
<Select.Divider />
<Select.Item value="bar">Bar</Select.Item>
</Select>
```

## License
Expand Down
38 changes: 38 additions & 0 deletions example/components/RFCItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useId } from 'react'
import { createHost, createSlot, getSlot } from 'create-slots/list'

type ItemProps = Omit<React.ComponentPropsWithoutRef<'li'>, 'value'> & {
value: string
}

const ItemTitle = createSlot<'h4'>()
const ItemDescription = createSlot<'div'>()

export const Item = (props: ItemProps) => {
const id = useId()

return createHost(props.children, (slots) => {
const titleSlot = getSlot(slots, ItemTitle)
const descriptionSlot = getSlot(slots, ItemDescription)
const titleId = titleSlot ? `${id}-title` : undefined
const descId = descriptionSlot ? `${id}-desc` : undefined

return (
<li aria-describedby={descId} aria-label={titleId} {...props}>
{titleSlot && (
<h4 id={titleId} ref={titleSlot.ref} {...titleSlot.props} />
)}
{descriptionSlot && (
<div
id={descId}
ref={descriptionSlot.ref}
{...descriptionSlot.props}
/>
)}
</li>
)
})
}

Item.Title = ItemTitle
Item.Description = ItemDescription
55 changes: 55 additions & 0 deletions example/components/RFCSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useRef, useState } from 'react'
import { createHost, createSlot, getSlotProps, isSlot } from 'create-slots/list'

import { Item } from './RFCItem'

const SelectItem = createSlot<typeof Item>()
const SelectDivider = createSlot('hr')

export const Select = (props: React.ComponentProps<'ul'>) => {
const [selected, setSelected] = useState<React.ReactNode>(null)
const indexRef = useRef(0)

return (
<div>
<div>Selected: {selected}</div>
{createHost(props.children, (slots) => {
indexRef.current = 0
return (
<ul {...props}>
{slots.map((slot) => {
if (isSlot(slot, SelectItem)) {
const itemProps = getSlotProps(slot)

return (
<Item
key={slot.key}
{...itemProps}
role="button"
tabIndex={0}
data-index={indexRef.current++}
aria-selected={itemProps.value === selected}
onClick={() => setSelected(itemProps.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
setSelected(itemProps.value)
}
}}
/>
)
}

return slot
})}
</ul>
)
})}
</div>
)
}

Select.Item = SelectItem
Select.Divider = SelectDivider

Select.Item.Title = Item.Title
Select.Item.Description = Item.Description
60 changes: 60 additions & 0 deletions example/components/SimpleField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useId, useState } from 'react'
import { createHost, createSlot } from 'create-slots'

const Description = (props: React.ComponentPropsWithoutRef<'div'>) => (
<div
{...props}
style={{ borderLeft: '4px solid lightgray', paddingLeft: 4 }}
/>
)

const FieldLabel = createSlot('label')
const FieldInput = createSlot('input')
const FieldDescription = createSlot(Description)

const StyledLabel = (props: React.ComponentPropsWithoutRef<'label'>) => (
<FieldLabel {...props} style={{ color: 'red' }} />
)

export const Field = (props: React.ComponentPropsWithoutRef<'div'>) => {
const id = useId()
const [value, setValue] = useState('')

if (value === 'a') return null

return (
<div {...props}>
{createHost(props.children, (Slots) => {
const labelProps = Slots.getProps(FieldLabel)
const inputProps = Slots.getProps(FieldInput)
const descriptionIdProps = Slots.getProps(FieldDescription)

const inputId = inputProps?.id || `${id}-label`
const descriptionId = descriptionIdProps ? `${id}-desc` : undefined

return (
<>
{labelProps && <label {...labelProps} htmlFor={inputId} />}
{inputProps && (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
id={inputId}
aria-describedby={descriptionId}
{...inputProps}
/>
)}
{descriptionIdProps && (
<Description id={descriptionId} {...descriptionIdProps} />
)}
</>
)
})}
</div>
)
}

Field.Label = FieldLabel
Field.Input = FieldInput
Field.Description = FieldDescription
Field.StyledLabel = StyledLabel
Loading