From 7291491f5945866192372c14590151e97151e872 Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Thu, 11 Aug 2022 11:17:01 +0100 Subject: [PATCH 1/7] WIP: add rfc --- example/components/RFCItem.tsx | 38 +++++++++++ example/components/RFCSelect.tsx | 60 +++++++++++++++++ example/components/SimpleField.tsx | 60 +++++++++++++++++ example/pages/index.tsx | 102 +++++++++++++++++++++-------- example/styles/Home.module.css | 2 +- package.json | 24 ++++++- rfc/package.json | 15 +++++ simple/package.json | 15 +++++ src/{list => }/ScanContext.tsx | 0 src/list/index.tsx | 2 +- src/rfc/SlotsManager.ts | 29 ++++++++ src/rfc/index.tsx | 94 ++++++++++++++++++++++++++ src/rfc/utils.ts | 34 ++++++++++ src/simple/SlotsManager.ts | 31 +++++++++ src/simple/index.tsx | 60 +++++++++++++++++ src/utils.ts | 3 +- 16 files changed, 539 insertions(+), 30 deletions(-) create mode 100644 example/components/RFCItem.tsx create mode 100644 example/components/RFCSelect.tsx create mode 100644 example/components/SimpleField.tsx create mode 100644 rfc/package.json create mode 100644 simple/package.json rename src/{list => }/ScanContext.tsx (100%) create mode 100644 src/rfc/SlotsManager.ts create mode 100644 src/rfc/index.tsx create mode 100644 src/rfc/utils.ts create mode 100644 src/simple/SlotsManager.ts create mode 100644 src/simple/index.tsx diff --git a/example/components/RFCItem.tsx b/example/components/RFCItem.tsx new file mode 100644 index 0000000..c6a8633 --- /dev/null +++ b/example/components/RFCItem.tsx @@ -0,0 +1,38 @@ +import React, { useId } from 'react' +import { createHost, createSlot, getSlot } from 'create-slots/rfc' + +type ItemProps = Omit, '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 ( +
  • + {titleSlot && ( +

    + )} + {descriptionSlot && ( +
    + )} +

  • + ) + }) +} + +Item.Title = ItemTitle +Item.Description = ItemDescription diff --git a/example/components/RFCSelect.tsx b/example/components/RFCSelect.tsx new file mode 100644 index 0000000..fe9c338 --- /dev/null +++ b/example/components/RFCSelect.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react' +import { createHost, createSlot } from 'create-slots/rfc' + +import { Item } from './RFCItem' + +const SelectItem = createSlot() +const SelectDivider = createSlot('hr') + +export const Select = (props: React.ComponentProps<'ul'>) => { + const [selected, setSelected] = useState(null) + + return ( +
    +
    Selected: {selected}
    + {createHost(props.children, (slots) => { + let index = 0 + return ( +
      + {slots.map((slot) => { + if (slot.type === SelectItem) { + const itemProps = slot.props + + return ( + { + setSelected(itemProps.value) + }, + onKeyDown: ( + event: React.KeyboardEvent + ) => { + if (event.key === 'Enter' || event.key === ' ') { + setSelected(itemProps.value) + } + }, + }} + /> + ) + } + + return slot + })} +
    + ) + })} +
    + ) +} + +Select.Item = SelectItem +Select.Divider = SelectDivider + +Select.Item.Title = Item.Title +Select.Item.Description = Item.Description diff --git a/example/components/SimpleField.tsx b/example/components/SimpleField.tsx new file mode 100644 index 0000000..b6ff1d3 --- /dev/null +++ b/example/components/SimpleField.tsx @@ -0,0 +1,60 @@ +import React, { useId, useState } from 'react' +import { createHost, createSlot } from 'create-slots/simple' + +const Description = (props: React.ComponentPropsWithoutRef<'div'>) => ( +
    +) + +const FieldLabel = createSlot('label') +const FieldInput = createSlot('input') +const FieldDescription = createSlot(Description) + +const StyledLabel = (props: React.ComponentPropsWithoutRef<'label'>) => ( + +) + +export const Field = (props: React.ComponentPropsWithoutRef<'div'>) => { + const id = useId() + const [value, setValue] = useState('') + + if (value === 'a') return null + + return ( +
    + {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 &&
    + ) +} + +Field.Label = FieldLabel +Field.Input = FieldInput +Field.Description = FieldDescription +Field.StyledLabel = StyledLabel diff --git a/example/pages/index.tsx b/example/pages/index.tsx index dc5f081..fb69ace 100755 --- a/example/pages/index.tsx +++ b/example/pages/index.tsx @@ -6,6 +6,8 @@ import styles from '../styles/Home.module.css' import { Field } from '../components/Field' import { StaticField } from '../components/StaticField' import { Select } from '../components/Select' +import { Field as SimpleField } from '../components/SimpleField' +import { Select as RFCSelect } from '../components/RFCSelect' const Home: NextPage = () => { const [count, setCount] = React.useState(0) @@ -27,6 +29,80 @@ const Home: NextPage = () => {
    +
    +

    RFC

    +
    + + + Foo + + + {count % 3 !== 2 && ( + <> + + Bar + + + + )} + + Baz + + count {count} + + + +
    +
    + +
    +

    List

    +
    + +
    +
    + +
    +

    Simple

    +
    + + + Label + {count % 3 !== 0 && ( + + Description {count} + + + Label + + Nested SimpleField {count} + + + + )} + +
    +
    +

    Dynamic

    @@ -71,32 +147,6 @@ const Home: NextPage = () => {
    - -
    -

    List

    -
    - -
    -
    diff --git a/example/styles/Home.module.css b/example/styles/Home.module.css index 5835f20..c81b9c2 100755 --- a/example/styles/Home.module.css +++ b/example/styles/Home.module.css @@ -64,7 +64,7 @@ border: 1px solid #eaeaea; border-radius: 10px; transition: color 0.15s ease, border-color 0.15s ease; - + width: 300px; } .card:hover, diff --git a/package.json b/package.json index db90be2..d12ea89 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,26 @@ "import": "./dist/static/index.mjs", "require": "./dist/static/index.js" } + }, + "./simple": { + "development": { + "import": "./dev/simple/index.mjs", + "require": "./dev/simple/index.js" + }, + "default": { + "import": "./dist/simple/index.mjs", + "require": "./dist/simple/index.js" + } + }, + "./rfc": { + "development": { + "import": "./dev/rfc/index.mjs", + "require": "./dev/rfc/index.js" + }, + "default": { + "import": "./dist/rfc/index.mjs", + "require": "./dist/rfc/index.js" + } } }, "sideEffects": false, @@ -60,7 +80,9 @@ "entry": [ "src/index.tsx", "src/list/index.tsx", - "src/static/index.tsx" + "src/static/index.tsx", + "src/simple/index.tsx", + "src/rfc/index.tsx" ], "format": [ "esm", diff --git a/rfc/package.json b/rfc/package.json new file mode 100644 index 0000000..e4f698a --- /dev/null +++ b/rfc/package.json @@ -0,0 +1,15 @@ +{ + "main": "../dist/rfc/index.js", + "module": "../dist/rfc/index.mjs", + "types": "../dist/rfc/index.d.ts", + "exports": { + "development": { + "import": "./../dev/rfc/index.mjs", + "require": "./../dev/rfc/index.js" + }, + "default": { + "import": "./../dist/rfc/index.mjs", + "require": "./../dist/rfc/index.js" + } + } +} diff --git a/simple/package.json b/simple/package.json new file mode 100644 index 0000000..d4321ee --- /dev/null +++ b/simple/package.json @@ -0,0 +1,15 @@ +{ + "main": "../dist/simple/index.js", + "module": "../dist/simple/index.mjs", + "types": "../dist/simple/index.d.ts", + "exports": { + "development": { + "import": "./../dev/simple/index.mjs", + "require": "./../dev/simple/index.js" + }, + "default": { + "import": "./../dist/simple/index.mjs", + "require": "./../dist/simple/index.js" + } + } +} diff --git a/src/list/ScanContext.tsx b/src/ScanContext.tsx similarity index 100% rename from src/list/ScanContext.tsx rename to src/ScanContext.tsx diff --git a/src/list/index.tsx b/src/list/index.tsx index 345834b..7fa1e4c 100644 --- a/src/list/index.tsx +++ b/src/list/index.tsx @@ -10,8 +10,8 @@ import React, { import { createSlotsContext, getComponentName, hoistStatics } from '../utils' import { DevChildren } from '../DevChildren' +import { ScanContext, ScanProvider } from '../ScanContext' import { createSlotsManager } from './SlotsManager' -import { ScanContext, ScanProvider } from './ScanContext' export type { GetPropsArgs } from './SlotsManager' const createSlots = >( diff --git a/src/rfc/SlotsManager.ts b/src/rfc/SlotsManager.ts new file mode 100644 index 0000000..3c934c3 --- /dev/null +++ b/src/rfc/SlotsManager.ts @@ -0,0 +1,29 @@ +import * as React from 'react' + +type Key = React.Key + +export const createSlotsManager = (onChange: (key: Key) => void) => { + const itemMap = new Map() + return { + register(key: Key, element: React.ReactElement) { + itemMap.set(key, element) + }, + update(key: Key, element: React.ReactElement) { + itemMap.set(key, element) + onChange?.(key) + }, + unmount(key: Key) { + itemMap.delete(key) + onChange?.(key) + }, + clear() { + itemMap.clear() + }, + has(key: Key) { + return itemMap.has(key) + }, + get() { + return Array.from(itemMap.values()) + }, + } +} diff --git a/src/rfc/index.tsx b/src/rfc/index.tsx new file mode 100644 index 0000000..392cf46 --- /dev/null +++ b/src/rfc/index.tsx @@ -0,0 +1,94 @@ +import * as React from 'react' + +import { createSlotsContext, getComponentName } from '../utils' +import { ScanContext, ScanProvider } from '../ScanContext' +import { createSlotsManager } from './SlotsManager' + +export * from '../utils' + +type Slots = ReturnType +type Callback = (Slots: React.ReactElement[]) => JSX.Element | null + +const SlotsContext = createSlotsContext(undefined) + +const Template = ({ children }: { children: () => ReturnType }) => { + return children() +} + +const createIdGenerator = (prefix = '') => { + let id = 0 + return () => `${prefix}_${id++}` +} + +const genSlotId = createIdGenerator('s') + +export const Host = ({ + children, + callback, +}: { + children: React.ReactNode + callback: Callback +}) => { + const forceUpdate = React.useReducer(() => [], [])[1] + const Slots = React.useMemo( + () => createSlotsManager(forceUpdate), + [forceUpdate] + ) + + return ( + <> + + {children} + + + + ) +} + +export const createHost = (children: React.ReactNode, callback: Callback) => { + return +} + +export const createSlot = (Fallback?: T) => { + const genId = createIdGenerator(genSlotId()) + + const Slot = React.forwardRef( + ({ $slot_key$: key, ...props }: any, ref: any) => { + const Slots = React.useContext(SlotsContext) + if (!Slots) return null + /* eslint-disable react-hooks/rules-of-hooks */ + const Scan = React.useContext(ScanContext) + + const element = + Slots.register(key, element) + React.useEffect(() => { + Slots.has(key) && Slots.update(key, element) + }) + React.useEffect(() => { + Slots.clear() + Scan.rescan() + return () => Slots.unmount(key) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [Slots]) + /* eslint-enable react-hooks/rules-of-hooks */ + + return null + } + ) as unknown as T + + // provide stable key in StrictMode + const ForwardRef = (props: any, ref: any) => { + const Slots = React.useContext(SlotsContext) + if (!Slots) return Fallback ? : null + + // eslint-disable-next-line react-hooks/rules-of-hooks + const [key] = React.useState(genId) + return + } + ForwardRef.displayName = Fallback + ? `Slot(${getComponentName(Fallback)})` + : 'Slot' + const SlotWithKey = React.forwardRef(ForwardRef) as unknown as T + + return SlotWithKey +} diff --git a/src/rfc/utils.ts b/src/rfc/utils.ts new file mode 100644 index 0000000..523cc6a --- /dev/null +++ b/src/rfc/utils.ts @@ -0,0 +1,34 @@ +import * as React from 'react' + +type SlotElement = React.ReactElement< + React.ComponentPropsWithRef, + T +> & { + ref: React.ComponentPropsWithRef['ref'] +} + +export const getSlots = ( + slots: React.ReactElement[], + slot: T +) => { + return slots.filter((x) => x.type === slot) as SlotElement[] +} + +export const getSlot = ( + slots: React.ReactElement[], + slot: T +) => { + return slots.find((x) => x.type === slot) as SlotElement | undefined +} + +export const getLastSlot = ( + slots: React.ReactElement[], + slot: T +) => { + for (let i = slots.length - 1; i >= 0; i--) { + if (slots[i].type === slot) { + return slots[i] as SlotElement + } + } + return undefined +} diff --git a/src/simple/SlotsManager.ts b/src/simple/SlotsManager.ts new file mode 100644 index 0000000..e6a0ffd --- /dev/null +++ b/src/simple/SlotsManager.ts @@ -0,0 +1,31 @@ +import * as React from 'react' + +type Slot = React.ElementType + +export const createSlotsManager = (onChange: (slot: Slot) => void) => { + const elementMap = new Map() + return { + register(slot: Slot, element: React.ReactElement) { + elementMap.set(slot, element) + }, + update(slot: Slot, element: React.ReactElement) { + elementMap.set(slot, element) + onChange(slot) + }, + unmount(slot: Slot) { + elementMap.delete(slot) + onChange(slot) + }, + get(slot: T) { + return elementMap.get(slot) as + | React.ReactElement, T> + | undefined + }, + getProps(slot: T) { + const element = elementMap.get(slot) + if (!element) return undefined + const { ref, props } = element as any + return (ref ? { ...props, ref } : props) as React.ComponentProps + }, + } +} diff --git a/src/simple/index.tsx b/src/simple/index.tsx new file mode 100644 index 0000000..80e023a --- /dev/null +++ b/src/simple/index.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' + +import { createSlotsContext, getComponentName } from '../utils' +import { createSlotsManager } from './SlotsManager' + +type Slots = ReturnType +type Callback = (Slots: Slots) => JSX.Element | null + +const SlotsContext = createSlotsContext(undefined) + +const Template = ({ children }: { children: () => ReturnType }) => { + return children() +} + +export const Host = ({ + children, + callback, +}: { + children: React.ReactNode + callback: Callback +}) => { + const forceUpdate = React.useReducer(() => [], [])[1] + const Slots = React.useMemo( + () => createSlotsManager(forceUpdate), + [forceUpdate] + ) + + return ( + <> + {children} + + + ) +} + +export const createHost = (children: React.ReactNode, callback: Callback) => { + return +} + +export const createSlot = (Fallback: T) => { + const ForwardRef = (props: any, ref: any) => { + const Slots = React.useContext(SlotsContext) + if (!Slots) return Fallback ? : null + + const element = + /* eslint-disable react-hooks/rules-of-hooks */ + React.useState(() => Slots.register(Slot, element)) + React.useEffect(() => Slots.update(Slot, element)) + React.useEffect(() => () => Slots.unmount(Slot), [Slots]) + /* eslint-enable react-hooks/rules-of-hooks */ + + return null + } + ForwardRef.displayName = Fallback + ? `Slot(${getComponentName(Fallback)})` + : 'Slot' + const Slot = React.forwardRef(ForwardRef) as unknown as T + + return Slot +} diff --git a/src/utils.ts b/src/utils.ts index 9ce2a67..bf22f6e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,7 +6,8 @@ export const createSlotsContext = (defaultValue: T) => { return context } -export const getComponentName = (Component: React.ComponentType) => { +export const getComponentName = (Component: React.ElementType) => { + if (typeof Component === 'string') return Component // istanbul ignore next return Component.displayName || Component.name || 'Component' } From bab1d2d6b9a4f95376fa0ab6d4a97eb629a476f5 Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Sat, 13 Aug 2022 00:13:31 +0100 Subject: [PATCH 2/7] add tests --- example/components/RFCSelect.tsx | 9 +- src/__fixtures__/RFCField.tsx | 40 +++++ src/__fixtures__/RFCSelect.tsx | 53 ++++++ src/__fixtures__/SimpleField.tsx | 69 ++++++++ src/__tests__/rfc-utils.test.tsx | 36 ++++ src/__tests__/rfc.test.tsx | 285 +++++++++++++++++++++++++++++++ src/__tests__/simple.test.tsx | 193 +++++++++++++++++++++ src/__tests__/ssr.test.tsx | 50 ++++++ src/rfc/SlotsManager.ts | 4 +- src/rfc/index.tsx | 17 +- src/rfc/utils.ts | 2 +- src/simple/index.tsx | 11 +- 12 files changed, 757 insertions(+), 12 deletions(-) create mode 100644 src/__fixtures__/RFCField.tsx create mode 100644 src/__fixtures__/RFCSelect.tsx create mode 100644 src/__fixtures__/SimpleField.tsx create mode 100644 src/__tests__/rfc-utils.test.tsx create mode 100644 src/__tests__/rfc.test.tsx create mode 100644 src/__tests__/simple.test.tsx diff --git a/example/components/RFCSelect.tsx b/example/components/RFCSelect.tsx index fe9c338..23a0c39 100644 --- a/example/components/RFCSelect.tsx +++ b/example/components/RFCSelect.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useRef, useState } from 'react' import { createHost, createSlot } from 'create-slots/rfc' import { Item } from './RFCItem' @@ -8,12 +8,13 @@ const SelectDivider = createSlot('hr') export const Select = (props: React.ComponentProps<'ul'>) => { const [selected, setSelected] = useState(null) + const indexRef = useRef(0) return (
    Selected: {selected}
    {createHost(props.children, (slots) => { - let index = 0 + indexRef.current = 0 return (
      {slots.map((slot) => { @@ -23,11 +24,11 @@ export const Select = (props: React.ComponentProps<'ul'>) => { return ( { setSelected(itemProps.value) diff --git a/src/__fixtures__/RFCField.tsx b/src/__fixtures__/RFCField.tsx new file mode 100644 index 0000000..ee3a010 --- /dev/null +++ b/src/__fixtures__/RFCField.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' + +import { createHost, createSlot, getSlot, getLastSlot, getSlots } from '../rfc' + +const Description = (props: React.ComponentPropsWithoutRef<'span'>) => ( + +) + +const FieldLabel = createSlot<'label'>() +const FieldInput = createSlot('input') +const FieldDescription = createSlot(Description) +const FieldIcon = createSlot('span') + +type FieldProps = React.ComponentPropsWithoutRef<'div'> + +export const Field = (props: FieldProps) => { + return createHost(props.children, (slots) => { + const labelSlot = getSlot(slots, FieldLabel) + const lastLabelSlot = getLastSlot(slots, FieldLabel) + const inputSlot = getSlot(slots, FieldInput) + const lastInputSlot = getLastSlot(slots, FieldInput) + const lastDescriptionSlot = getLastSlot(slots, FieldDescription) + const icons = getSlots(slots, FieldIcon) + return ( +
      + {lastLabelSlot &&
      + ) + }) +} + +Field.Label = FieldLabel +Field.Input = FieldInput +Field.Description = FieldDescription +Field.Icon = FieldIcon diff --git a/src/__fixtures__/RFCSelect.tsx b/src/__fixtures__/RFCSelect.tsx new file mode 100644 index 0000000..277f2ff --- /dev/null +++ b/src/__fixtures__/RFCSelect.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' + +import { createHost, createSlot } from '../rfc' + +const Divider = (props: React.ComponentPropsWithoutRef<'hr'>) => ( +
      +) + +const SelectItem = createSlot('li') +const SelectDivider = createSlot(Divider) + +export const Select = (props: React.ComponentPropsWithoutRef<'ul'>) => { + const [selected, setSelected] = React.useState() + const indexRef = React.useRef(0) + + return ( +
      +
      Selected: {selected?.props.value ?? ''}
      + {createHost(props.children, (slots) => { + indexRef.current = 0 + return ( +
        + {slots.map((slot) => { + if (slot.type === SelectItem) { + const itemProps = slot.props + + return ( +
      • { + setSelected(slot) + }, + }} + /> + ) + } + + return slot + })} +
      + ) + })} +
      + ) +} + +Select.Item = SelectItem +Select.Divider = SelectDivider diff --git a/src/__fixtures__/SimpleField.tsx b/src/__fixtures__/SimpleField.tsx new file mode 100644 index 0000000..83ce4c5 --- /dev/null +++ b/src/__fixtures__/SimpleField.tsx @@ -0,0 +1,69 @@ +import * as React from 'react' + +import { createHost, createSlot } from '../simple' + +const Description = (props: React.ComponentPropsWithoutRef<'span'>) => ( + +) + +const FieldLabel = createSlot<'label'>() +const FieldInput = createSlot('input') +const FieldDescription = createSlot(Description) +const FieldIcon = createSlot('span') + +// const createFill =

      (name: P) => { +// const FillComponent = React.forwardRef((props, ref) => { +// const Slots = useSlots() +// const [originalProps] = React.useState(() => Slots.getProps(name)) + +// React.useEffect(() => Slots.update(name, { ...props, ref })) +// React.useEffect( +// () => () => { +// originalProps ? Slots.update(name, originalProps) : Slots.unmount(name) +// }, +// [Slots, originalProps] +// ) + +// return null +// }) as unknown as typeof SlotComponents[P] +// return FillComponent +// } + +type FieldProps = React.ComponentPropsWithoutRef<'div'> + +export const Field = (props: FieldProps) => { + const id = ':r0:' + const descriptionId = ':r1:' + + return createHost(props.children, (Slots) => { + const labelProps = Slots.getProps(FieldLabel) + const inputProps = Slots.getProps(FieldInput) + const descriptionProps = Slots.getProps(FieldDescription) + const iconProps = Slots.getProps(FieldIcon) + return ( +

      + {labelProps &&
      + ) + }) +} + +Field.Label = FieldLabel +Field.Input = FieldInput +Field.Description = FieldDescription +Field.Icon = FieldIcon diff --git a/src/__tests__/rfc-utils.test.tsx b/src/__tests__/rfc-utils.test.tsx new file mode 100644 index 0000000..f84b489 --- /dev/null +++ b/src/__tests__/rfc-utils.test.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' + +import { create } from '../__fixtures__/utils' +import { Field } from '../__fixtures__/RFCField' + +test('render slots', () => { + const instance = create( + + + + Label 1 + Label 2 + Icon 1 + Icon 2 + + ) + expect(instance).toMatchInlineSnapshot(` +
      + + + + Icon 1 + + + Icon 2 + + +
      + `) +}) diff --git a/src/__tests__/rfc.test.tsx b/src/__tests__/rfc.test.tsx new file mode 100644 index 0000000..edfa319 --- /dev/null +++ b/src/__tests__/rfc.test.tsx @@ -0,0 +1,285 @@ +import * as React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' + +import { create } from '../__fixtures__/utils' +import { Select } from '../__fixtures__/RFCSelect' + +test('render slots', () => { + const instance = create( + + ) + expect(instance).toMatchInlineSnapshot(` +
      +
      + Selected: +
      +
        +
      • + Foo +
      • +
        +
      • + Bar +
      • +
      +
      +`) + + // insert item + instance.update( + + ) + expect(instance).toMatchInlineSnapshot(` +
      +
      + Selected: +
      +
        +
      • + Foo +
      • +
        +
      • + Baz +
      • +
        +
      • + Bar +
      • +
      +
      +`) + + // remove item + instance.update( + + ) + expect(instance).toMatchInlineSnapshot(` +
      +
      + Selected: +
      +
        +
      • + Foo +
      • +
        +
      • + Bar +
      • +
      +
      +`) + + // update item + instance.update( + + ) + expect(instance).toMatchInlineSnapshot(` +
      +
      + Selected: +
      +
        +
      • + FooFoo +
      • +
        +
      • + Bar +
      • +
      +
      +`) + + // nested slots + instance.update( + + FooFoo + + Bar + + + + Bar + + ) + + expect(instance).toMatchInlineSnapshot(` +
      +
      + Selected: +
      +
        +
      • +
        +
        + Selected: +
        +
          +
        • + FooFoo +
        • +
          +
        • + Bar +
        • +
        +
        +
      • +
        +
      • + Bar +
      • +
      +
      +`) +}) + +test('ref', () => { + const ref = { current: null } + render( + + ) + + expect(ref.current).toMatchInlineSnapshot(` +
    • + Foo +
    • +`) +}) + +test('interaction', () => { + render( + + ) + + fireEvent.click(screen.getAllByRole('listitem')[0]) + expect(screen.getByText('Selected: foo')).not.toBeNull() + + fireEvent.click(screen.getAllByRole('listitem')[1]) + expect(screen.getByText('Selected: bar')).not.toBeNull() +}) + +test('dev warning', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation() + render( + + ) + expect(warn).toHaveBeenCalledTimes(0) + + // @ts-ignore + process.env.NODE_ENV = 'development' + render( + + ) + expect(warn).toHaveBeenCalledTimes(1) + expect(warn).toHaveBeenCalledWith( + 'Unwrapped children found in "Host", either wrap them in subcomponents or remove' + ) +}) diff --git a/src/__tests__/simple.test.tsx b/src/__tests__/simple.test.tsx new file mode 100644 index 0000000..6a3acf5 --- /dev/null +++ b/src/__tests__/simple.test.tsx @@ -0,0 +1,193 @@ +import * as React from 'react' +import { render } from '@testing-library/react' + +import { create } from '../__fixtures__/utils' +import { Field } from '../__fixtures__/SimpleField' + +test('render slots', () => { + const instance = create( + + Label + + - + Description + + ) + expect(instance).toMatchInlineSnapshot(` +
      + + +
      + + - + + + Description + +
      +
      + `) + + // arbitrary order + instance.update( + + - + + Description + Label + + ) + expect(instance).toMatchInlineSnapshot(` +
      + + +
      + + - + + + Description + +
      +
      + `) + + // dynamic content + instance.update( + + Label + + + ) + + expect(instance).toMatchInlineSnapshot(` +
      + + +
      + `) + + // nested slots + instance.update( + + Label + + + + Label + + + + + ) + + expect(instance).toMatchInlineSnapshot(` +
      + + +
      + +
      + + +
      +
      +
      +
      + `) +}) + +test('ref', () => { + const ref = { current: null } + render( + + Label + + - + Description + + ) + + expect(ref.current).toMatchInlineSnapshot(` + + `) +}) + +test('dev warning', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation() + render( + + Label + Input + + ) + expect(warn).toHaveBeenCalledTimes(0) + + // @ts-ignore + process.env.NODE_ENV = 'development' + render( + + Label + Input + + ) + expect(warn).toHaveBeenCalledTimes(1) + expect(warn).toHaveBeenCalledWith( + 'Unwrapped children found in "Host", either wrap them in subcomponents or remove' + ) +}) + +test('without host', () => { + const instance = create(Label) + expect(instance).toMatchInlineSnapshot(`null`) + + instance.update(Description) + expect(instance).toMatchInlineSnapshot(` + + Description + + `) +}) diff --git a/src/__tests__/ssr.test.tsx b/src/__tests__/ssr.test.tsx index 3673bd7..0d570d9 100644 --- a/src/__tests__/ssr.test.tsx +++ b/src/__tests__/ssr.test.tsx @@ -8,6 +8,8 @@ import { renderToStaticMarkup } from 'react-dom/server' import { Field } from '../__fixtures__/Field' import { StaticField } from '../__fixtures__/StaticField' import { Select } from '../__fixtures__/Select' +import { Select as RFCSelect } from '../__fixtures__/RFCSelect' +import { Field as SimpleField } from '../__fixtures__/SimpleField' test('default SSR', () => { const markup = renderToStaticMarkup( @@ -91,3 +93,51 @@ test('list SSR', () => { `"
      Selected:
      • Foo

      • Bar
      "` ) }) + +test('rfc SSR', () => { + const markup = renderToStaticMarkup( + + Foo + + Bar + + ) + expect(markup).toMatchInlineSnapshot( + `"
      Selected:
      • Foo

      • Bar
      "` + ) +}) + +test('simple SSR', () => { + const markup = renderToStaticMarkup( + + Label + + + + Label + + + + + ) + + expect(markup).toMatchInlineSnapshot( + `"
      "` + ) + + // arbitrary order + const markup1 = renderToStaticMarkup( + + + + Label + + + + Label + + + ) + + expect(markup1).toEqual(markup) +}) diff --git a/src/rfc/SlotsManager.ts b/src/rfc/SlotsManager.ts index 3c934c3..edf7160 100644 --- a/src/rfc/SlotsManager.ts +++ b/src/rfc/SlotsManager.ts @@ -1,5 +1,7 @@ import * as React from 'react' +import { SlotElement } from './utils' + type Key = React.Key export const createSlotsManager = (onChange: (key: Key) => void) => { @@ -23,7 +25,7 @@ export const createSlotsManager = (onChange: (key: Key) => void) => { return itemMap.has(key) }, get() { - return Array.from(itemMap.values()) + return Array.from(itemMap.values()) as SlotElement[] }, } } diff --git a/src/rfc/index.tsx b/src/rfc/index.tsx index 392cf46..a1479f6 100644 --- a/src/rfc/index.tsx +++ b/src/rfc/index.tsx @@ -1,13 +1,15 @@ import * as React from 'react' import { createSlotsContext, getComponentName } from '../utils' +import { DevChildren } from '../DevChildren' import { ScanContext, ScanProvider } from '../ScanContext' import { createSlotsManager } from './SlotsManager' +import { SlotElement } from './utils' -export * from '../utils' +export * from './utils' type Slots = ReturnType -type Callback = (Slots: React.ReactElement[]) => JSX.Element | null +type Callback = (slots: SlotElement[]) => JSX.Element | null const SlotsContext = createSlotsContext(undefined) @@ -15,7 +17,7 @@ const Template = ({ children }: { children: () => ReturnType }) => { return children() } -const createIdGenerator = (prefix = '') => { +const createIdGenerator = (prefix: string) => { let id = 0 return () => `${prefix}_${id++}` } @@ -38,7 +40,13 @@ export const Host = ({ return ( <> - {children} + + {process.env.NODE_ENV === 'development' ? ( + {children} + ) : ( + children + )} + @@ -55,6 +63,7 @@ export const createSlot = (Fallback?: T) => { const Slot = React.forwardRef( ({ $slot_key$: key, ...props }: any, ref: any) => { const Slots = React.useContext(SlotsContext) + // istanbul ignore next if (!Slots) return null /* eslint-disable react-hooks/rules-of-hooks */ const Scan = React.useContext(ScanContext) diff --git a/src/rfc/utils.ts b/src/rfc/utils.ts index 523cc6a..7fd1971 100644 --- a/src/rfc/utils.ts +++ b/src/rfc/utils.ts @@ -1,6 +1,6 @@ import * as React from 'react' -type SlotElement = React.ReactElement< +export type SlotElement = React.ReactElement< React.ComponentPropsWithRef, T > & { diff --git a/src/simple/index.tsx b/src/simple/index.tsx index 80e023a..46f481e 100644 --- a/src/simple/index.tsx +++ b/src/simple/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { createSlotsContext, getComponentName } from '../utils' +import { DevChildren } from '../DevChildren' import { createSlotsManager } from './SlotsManager' type Slots = ReturnType @@ -27,7 +28,13 @@ export const Host = ({ return ( <> - {children} + + {process.env.NODE_ENV === 'development' ? ( + {children} + ) : ( + children + )} + ) @@ -37,7 +44,7 @@ export const createHost = (children: React.ReactNode, callback: Callback) => { return } -export const createSlot = (Fallback: T) => { +export const createSlot = (Fallback?: T) => { const ForwardRef = (props: any, ref: any) => { const Slots = React.useContext(SlotsContext) if (!Slots) return Fallback ? : null From 911239990fd5748e331cb2cfe83d5e2bd3b1f913 Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Sun, 14 Aug 2022 16:09:02 +0100 Subject: [PATCH 3/7] Host -> HostSlots --- src/__fixtures__/RFCField.tsx | 25 ++++++++++++++++++------- src/__tests__/rfc.test.tsx | 2 +- src/__tests__/simple.test.tsx | 2 +- src/rfc/index.tsx | 6 +++--- src/rfc/utils.ts | 10 ++++++++++ src/simple/index.tsx | 6 +++--- 6 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/__fixtures__/RFCField.tsx b/src/__fixtures__/RFCField.tsx index ee3a010..2af0b36 100644 --- a/src/__fixtures__/RFCField.tsx +++ b/src/__fixtures__/RFCField.tsx @@ -1,6 +1,13 @@ import * as React from 'react' -import { createHost, createSlot, getSlot, getLastSlot, getSlots } from '../rfc' +import { + createHost, + createSlot, + getSlot, + getLastSlot, + getSlots, + getSlotProps, +} from '../rfc' const Description = (props: React.ComponentPropsWithoutRef<'span'>) => ( @@ -16,19 +23,23 @@ type FieldProps = React.ComponentPropsWithoutRef<'div'> export const Field = (props: FieldProps) => { return createHost(props.children, (slots) => { const labelSlot = getSlot(slots, FieldLabel) - const lastLabelSlot = getLastSlot(slots, FieldLabel) + const lastLabelProps = getSlotProps(getLastSlot(slots, FieldLabel)) const inputSlot = getSlot(slots, FieldInput) - const lastInputSlot = getLastSlot(slots, FieldInput) - const lastDescriptionSlot = getLastSlot(slots, FieldDescription) + const lastInputProps = getSlotProps(getLastSlot(slots, FieldInput)) + const descriptionSlot = getSlot(slots, FieldDescription) + const lastDescriptionProps = getSlotProps( + getLastSlot(slots, FieldDescription) + ) const icons = getSlots(slots, FieldIcon) return (
      - {lastLabelSlot &&
      ) }) diff --git a/src/__tests__/rfc.test.tsx b/src/__tests__/rfc.test.tsx index edfa319..918b74e 100644 --- a/src/__tests__/rfc.test.tsx +++ b/src/__tests__/rfc.test.tsx @@ -280,6 +280,6 @@ test('dev warning', () => { ) expect(warn).toHaveBeenCalledTimes(1) expect(warn).toHaveBeenCalledWith( - 'Unwrapped children found in "Host", either wrap them in subcomponents or remove' + 'Unwrapped children found in "HostSlots", either wrap them in slots or remove' ) }) diff --git a/src/__tests__/simple.test.tsx b/src/__tests__/simple.test.tsx index 6a3acf5..64fc5ec 100644 --- a/src/__tests__/simple.test.tsx +++ b/src/__tests__/simple.test.tsx @@ -176,7 +176,7 @@ test('dev warning', () => { ) expect(warn).toHaveBeenCalledTimes(1) expect(warn).toHaveBeenCalledWith( - 'Unwrapped children found in "Host", either wrap them in subcomponents or remove' + 'Unwrapped children found in "HostSlots", either wrap them in slots or remove' ) }) diff --git a/src/rfc/index.tsx b/src/rfc/index.tsx index a1479f6..497cfff 100644 --- a/src/rfc/index.tsx +++ b/src/rfc/index.tsx @@ -24,7 +24,7 @@ const createIdGenerator = (prefix: string) => { const genSlotId = createIdGenerator('s') -export const Host = ({ +export const HostSlots = ({ children, callback, }: { @@ -42,7 +42,7 @@ export const Host = ({ {process.env.NODE_ENV === 'development' ? ( - {children} + {children} ) : ( children )} @@ -54,7 +54,7 @@ export const Host = ({ } export const createHost = (children: React.ReactNode, callback: Callback) => { - return + return } export const createSlot = (Fallback?: T) => { diff --git a/src/rfc/utils.ts b/src/rfc/utils.ts index 7fd1971..0970256 100644 --- a/src/rfc/utils.ts +++ b/src/rfc/utils.ts @@ -32,3 +32,13 @@ export const getLastSlot = ( } return undefined } + +export const getSlotProps = (slotElement?: T) => { + if (!slotElement) return undefined + + type Props = React.ComponentPropsWithRef< + T extends SlotElement ? P : never + > + const { key, ref, props } = slotElement + return { ...props, key, ref } as Props +} diff --git a/src/simple/index.tsx b/src/simple/index.tsx index 46f481e..1d8c57e 100644 --- a/src/simple/index.tsx +++ b/src/simple/index.tsx @@ -13,7 +13,7 @@ const Template = ({ children }: { children: () => ReturnType }) => { return children() } -export const Host = ({ +export const HostSlots = ({ children, callback, }: { @@ -30,7 +30,7 @@ export const Host = ({ <> {process.env.NODE_ENV === 'development' ? ( - {children} + {children} ) : ( children )} @@ -41,7 +41,7 @@ export const Host = ({ } export const createHost = (children: React.ReactNode, callback: Callback) => { - return + return } export const createSlot = (Fallback?: T) => { From 6e65aaeb7e9536f92df877ffbb92bed443795a8f Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Mon, 15 Aug 2022 17:05:35 +0100 Subject: [PATCH 4/7] add createFill in test --- src/__fixtures__/SimpleField.tsx | 74 ++++++++++++++++++-------------- src/__tests__/simple.test.tsx | 62 ++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 33 deletions(-) diff --git a/src/__fixtures__/SimpleField.tsx b/src/__fixtures__/SimpleField.tsx index 83ce4c5..07c1532 100644 --- a/src/__fixtures__/SimpleField.tsx +++ b/src/__fixtures__/SimpleField.tsx @@ -11,23 +11,26 @@ const FieldInput = createSlot('input') const FieldDescription = createSlot(Description) const FieldIcon = createSlot('span') -// const createFill =

      (name: P) => { -// const FillComponent = React.forwardRef((props, ref) => { -// const Slots = useSlots() -// const [originalProps] = React.useState(() => Slots.getProps(name)) +type Slots = Parameters[1]>[0] +const FillContext = React.createContext(undefined) -// React.useEffect(() => Slots.update(name, { ...props, ref })) -// React.useEffect( -// () => () => { -// originalProps ? Slots.update(name, originalProps) : Slots.unmount(name) -// }, -// [Slots, originalProps] -// ) +const createFill = (Slot: T) => { + const FillComponent = React.forwardRef((props: any, ref) => { + const Slots = React.useContext(FillContext)! + const [original] = React.useState(() => Slots.get(Slot)) -// return null -// }) as unknown as typeof SlotComponents[P] -// return FillComponent -// } + React.useEffect(() => Slots.update(Slot, )) + React.useEffect( + () => () => { + original ? Slots.update(Slot, original) : Slots.unmount(Slot) + }, + [Slots, original] + ) + + return null + }) as unknown as T + return FillComponent +} type FieldProps = React.ComponentPropsWithoutRef<'div'> @@ -41,24 +44,28 @@ export const Field = (props: FieldProps) => { const descriptionProps = Slots.getProps(FieldDescription) const iconProps = Slots.getProps(FieldIcon) return ( -

      - {labelProps &&
      + +
      + {labelProps && ( +
      +
      ) }) } @@ -67,3 +74,4 @@ Field.Label = FieldLabel Field.Input = FieldInput Field.Description = FieldDescription Field.Icon = FieldIcon +Field.LabelFill = createFill(FieldLabel) diff --git a/src/__tests__/simple.test.tsx b/src/__tests__/simple.test.tsx index 64fc5ec..e246aee 100644 --- a/src/__tests__/simple.test.tsx +++ b/src/__tests__/simple.test.tsx @@ -134,6 +134,68 @@ test('render slots', () => {
    `) + // filling slots + instance.update( + + Label + + + Description + Filled Label + + + ) + + expect(instance).toMatchInlineSnapshot(` +
    + + +
    + + Description + +
    +
    + `) + + // unmount filling slots + instance.update( + + Label + + Description + + ) + + expect(instance).toMatchInlineSnapshot(` +
    + + +
    + + Description + +
    +
    + `) }) test('ref', () => { From 0a9c06e3e524b10df3822e395ab9553cfa75603e Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Mon, 15 Aug 2022 23:36:40 +0100 Subject: [PATCH 5/7] add isSlot util --- src/__fixtures__/RFCSelect.tsx | 10 +++------- src/rfc/utils.ts | 7 +++++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/__fixtures__/RFCSelect.tsx b/src/__fixtures__/RFCSelect.tsx index 277f2ff..4c517ed 100644 --- a/src/__fixtures__/RFCSelect.tsx +++ b/src/__fixtures__/RFCSelect.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { createHost, createSlot } from '../rfc' +import { createHost, createSlot, isSlot, getSlotProps } from '../rfc' const Divider = (props: React.ComponentPropsWithoutRef<'hr'>) => (
    @@ -21,14 +21,10 @@ export const Select = (props: React.ComponentPropsWithoutRef<'ul'>) => { return (
      {slots.map((slot) => { - if (slot.type === SelectItem) { - const itemProps = slot.props - + if (isSlot(slot, SelectItem)) { return (
    • ( return undefined } +export const isSlot = ( + slotElement: SlotElement, + slot: T +): slotElement is SlotElement => { + return slotElement.type === slot +} + export const getSlotProps = (slotElement?: T) => { if (!slotElement) return undefined From 0b2d140422203c1dded6a5f9c5b7d12647a32566 Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Tue, 16 Aug 2022 01:15:38 +0100 Subject: [PATCH 6/7] update after rebase --- src/__tests__/rfc.test.tsx | 4 ++-- src/__tests__/simple.test.tsx | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/__tests__/rfc.test.tsx b/src/__tests__/rfc.test.tsx index 918b74e..2b4cfca 100644 --- a/src/__tests__/rfc.test.tsx +++ b/src/__tests__/rfc.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' -import { render, screen, fireEvent } from '@testing-library/react' +import { screen, fireEvent } from '@testing-library/react' -import { create } from '../__fixtures__/utils' +import { create, render } from '../__fixtures__/utils' import { Select } from '../__fixtures__/RFCSelect' test('render slots', () => { diff --git a/src/__tests__/simple.test.tsx b/src/__tests__/simple.test.tsx index e246aee..88cfa0c 100644 --- a/src/__tests__/simple.test.tsx +++ b/src/__tests__/simple.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react' -import { render } from '@testing-library/react' -import { create } from '../__fixtures__/utils' +import { create, render } from '../__fixtures__/utils' import { Field } from '../__fixtures__/SimpleField' test('render slots', () => { From 3a6234f3d4c2fa70205113b028faf919870bf1f5 Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Tue, 16 Aug 2022 02:28:23 +0100 Subject: [PATCH 7/7] reexport --- README.md | 117 +++++++++++++++++------------ example/components/RFCItem.tsx | 2 +- example/components/RFCSelect.tsx | 28 +++---- example/components/SimpleField.tsx | 2 +- package.json | 24 +----- rfc/package.json | 15 ---- simple/package.json | 15 ---- src/__fixtures__/RFCSelect.tsx | 17 ++--- src/index.tsx | 3 + src/list/index.tsx | 3 + src/rfc/utils.ts | 16 ++-- 11 files changed, 106 insertions(+), 136 deletions(-) delete mode 100644 rfc/package.json delete mode 100644 simple/package.json diff --git a/README.md b/README.md index 0fdd1e5..9a91067 100644 --- a/README.md +++ b/README.md @@ -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 = (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 ( -
      - {Slots.render('Label', { htmlFor: inputId })} - {Slots.render('Input', { - id: inputId, - 'aria-describedby': descriptionId, - })} - {Slots.render('Description', { id: descriptionId })} -
      - ) + return createHost(props.children, (Slots) => { + const labelProps = Slots.getProps(FieldLabel) + const inputProps = Slots.getProps(FieldInput) + const inputId = inputProps?.id || id + + return ( +
      + {labelProps &&
      + ) + }) } -export const Field = Object.assign(createHost(FieldBase), SlotComponents) +Field.Label = FieldLabel +Field.Input = FieldInput +Field.Description = FieldDescription ``` 2. Use it @@ -57,43 +59,62 @@ export const Field = Object.assign(createHost(FieldBase), SlotComponents) ``` -### 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> = (props) => { - const [selected, setSelected] = useState(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> + +const Select = (props: SelectProps) => { + const [selected, setSelected] = useState() + const indexRef = React.useRef(0) return (
      -
      Selected: {selected}
      -
        {slotItems}
      +
      Selected: {selected ?? ''}
      + {createHost(props.children, (slots) => { + indexRef.current = 0 + return ( +
        + {slots.map((slot) => { + if (isSlot(slot, SelectItem)) { + const slotProps = getSlotProps(slot) + return ( +
      • setSelected(slotProps.value as string)} + /> + ) + } + + return slot + })} +
      + ) + })}
      ) } -export const Select = Object.assign(createHost(SelectBase), SlotComponents) +Select.Item = SelectItem +Select.Divider = SelectDivider +``` + +2. Use it + +```tsx + ``` ## License diff --git a/example/components/RFCItem.tsx b/example/components/RFCItem.tsx index c6a8633..d0d6228 100644 --- a/example/components/RFCItem.tsx +++ b/example/components/RFCItem.tsx @@ -1,5 +1,5 @@ import React, { useId } from 'react' -import { createHost, createSlot, getSlot } from 'create-slots/rfc' +import { createHost, createSlot, getSlot } from 'create-slots/list' type ItemProps = Omit, 'value'> & { value: string diff --git a/example/components/RFCSelect.tsx b/example/components/RFCSelect.tsx index 23a0c39..4ed3b88 100644 --- a/example/components/RFCSelect.tsx +++ b/example/components/RFCSelect.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState } from 'react' -import { createHost, createSlot } from 'create-slots/rfc' +import { createHost, createSlot, getSlotProps, isSlot } from 'create-slots/list' import { Item } from './RFCItem' @@ -18,28 +18,22 @@ export const Select = (props: React.ComponentProps<'ul'>) => { return (
        {slots.map((slot) => { - if (slot.type === SelectItem) { - const itemProps = slot.props + if (isSlot(slot, SelectItem)) { + const itemProps = getSlotProps(slot) return ( { + 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) - }, - onKeyDown: ( - event: React.KeyboardEvent - ) => { - if (event.key === 'Enter' || event.key === ' ') { - setSelected(itemProps.value) - } - }, + } }} /> ) diff --git a/example/components/SimpleField.tsx b/example/components/SimpleField.tsx index b6ff1d3..3eddd9b 100644 --- a/example/components/SimpleField.tsx +++ b/example/components/SimpleField.tsx @@ -1,5 +1,5 @@ import React, { useId, useState } from 'react' -import { createHost, createSlot } from 'create-slots/simple' +import { createHost, createSlot } from 'create-slots' const Description = (props: React.ComponentPropsWithoutRef<'div'>) => (
        ) => { - const [selected, setSelected] = React.useState() + const [selected, setSelected] = React.useState() const indexRef = React.useRef(0) return (
        -
        Selected: {selected?.props.value ?? ''}
        +
        Selected: {selected ?? ''}
        {createHost(props.children, (slots) => { indexRef.current = 0 return (
          {slots.map((slot) => { if (isSlot(slot, SelectItem)) { + const slotProps = getSlotProps(slot) return (
        • { - setSelected(slot) - }, - }} + {...slotProps} + data-index={indexRef.current++} + aria-selected={slotProps.value === selected} + onClick={() => setSelected(slotProps.value as string)} /> ) } diff --git a/src/index.tsx b/src/index.tsx index cb41c1b..2003f05 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,6 +12,7 @@ import { createSlotsContext, getComponentName, hoistStatics } from './utils' import { createSlotsManager } from './SlotsManager' import { DevChildren } from './DevChildren' +/** @deprecated */ const createSlots = >( components: T ) => { @@ -68,3 +69,5 @@ const createSlots = >( } export default createSlots + +export * from './simple' diff --git a/src/list/index.tsx b/src/list/index.tsx index 7fa1e4c..9f55dbe 100644 --- a/src/list/index.tsx +++ b/src/list/index.tsx @@ -14,6 +14,7 @@ import { ScanContext, ScanProvider } from '../ScanContext' import { createSlotsManager } from './SlotsManager' export type { GetPropsArgs } from './SlotsManager' +/** @deprecated */ const createSlots = >( components: T ) => { @@ -89,3 +90,5 @@ const createSlots = >( } export default createSlots + +export * from '../rfc' diff --git a/src/rfc/utils.ts b/src/rfc/utils.ts index 0ec7fbd..721d4d4 100644 --- a/src/rfc/utils.ts +++ b/src/rfc/utils.ts @@ -40,12 +40,16 @@ export const isSlot = ( return slotElement.type === slot } -export const getSlotProps = (slotElement?: T) => { - if (!slotElement) return undefined +export const getSlotProps = < + T extends SlotElement | undefined, + Props = T extends undefined + ? undefined + : React.ComponentPropsWithRef ? P : never> +>( + slotElement: T +): Props => { + if (!slotElement) return undefined as any - type Props = React.ComponentPropsWithRef< - T extends SlotElement ? P : never - > const { key, ref, props } = slotElement - return { ...props, key, ref } as Props + return { ...props, key, ref } }