[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. luke-ui.netlify.app/
0

Configure Feed

Select the types of activity you want to include in your feed.

Consolidate polymorphic APIs onto elementType and render (#123)

* Consolidate polymorphic APIs onto elementType and render

Drop the hand-rolled `as` prop (used only by LoadingSkeleton) in favour
of `elementType`, the react-aria-components convention already used by
Text, Heading, and Numeral. `as` and `elementType` did the same job
(pick the DOM tag); this removes the duplicate, non-idiomatic path.

Also extract Box's local `BoxRender` type into a shared, documented
`RenderProp` type. react-aria-components does not publicly export its
render-function type, so this is the canonical Luke UI definition,
stopping the next component from inventing another render-prop variant.

Net API surface: `elementType` = which tag, `render` = take over
rendering. Two orthogonal, RAC-aligned APIs, no bespoke `as`.

- LoadingSkeleton: `as` -> `elementType` (component, stories, docs, example)
- heading-context: fix stale JSDoc example that referenced `as`
- Box: use shared RenderProp<'div'> instead of local BoxRender
- add packages/@luke-ui/react/src/types/render-prop.ts

* Reflow loading-skeleton docs prose to satisfy formatter

authored by

Luke Bennett and committed by
GitHub
(Jul 16, 2026, 11:35 AM +1000) f97faba4 970b317e

+25 -12
+2 -2
apps/docs/content/docs/components/feedback/loading-skeleton.mdx
··· 40 40 41 41 ## Element type 42 42 43 - `LoadingSkeleton` renders a `span` by default. Use `as` when the surrounding markup needs another 44 - element. 43 + `LoadingSkeleton` renders a `span` by default. Use `elementType` when the surrounding markup needs 44 + another element. 45 45 46 46 <ExampleBlock 47 47 src="loading-skeleton/element"
+1 -1
apps/docs/src/examples/loading-skeleton/element.tsx
··· 9 9 style={{ alignItems: 'flex-start', display: 'flex', flexDirection: 'column', gap: '1rem' }} 10 10 > 11 11 <ul> 12 - <LoadingSkeleton as="li" isLoading={isLoading}> 12 + <LoadingSkeleton elementType="li" isLoading={isLoading}> 13 13 List item placeholder 14 14 </LoadingSkeleton> 15 15 </ul>
+3 -4
packages/@luke-ui/react/src/box/index.tsx
··· 1 - import type { ComponentPropsWithRef, JSX, ReactElement } from 'react'; 1 + import type { ComponentPropsWithRef, JSX } from 'react'; 2 2 import type { SprinklesProps } from '../styles/index.js'; 3 3 import { createSprinkles } from '../styles/index.js'; 4 4 import type { DistributiveOmit } from '../types/distributive-omit.js'; 5 5 import type { Prettify } from '../types/prettify.js'; 6 + import type { RenderProp } from '../types/render-prop.js'; 6 7 import { mergeProps } from '../utils/index.js'; 7 8 8 - type BoxRender = (props: ComponentPropsWithRef<'div'>, renderProps: undefined) => ReactElement; 9 - 10 9 interface _BoxProps extends ComponentPropsWithRef<'div'>, SprinklesProps { 11 10 /** Renders a compatible custom `div` while carrying Box's DOM props and generated styles. */ 12 - render?: BoxRender; 11 + render?: RenderProp<'div'>; 13 12 } 14 13 15 14 /**
+1 -1
packages/@luke-ui/react/src/heading-context/index.tsx
··· 82 82 * // Using render prop to access level and element 83 83 * <HeadingLevels base={2}> 84 84 * {({ level, element }) => ( 85 - * <Heading as={element} level={level}> 85 + * <Heading elementType={element} level={level}> 86 86 * Dynamic Heading 87 87 * </Heading> 88 88 * )}
+2 -2
packages/@luke-ui/react/src/loading-skeleton/index.tsx
··· 28 28 * Element rendered while loading. 29 29 * @default 'span' 30 30 */ 31 - as?: ElementType; 31 + elementType?: ElementType; 32 32 /** 33 33 * Sets the semantic corner radius of the skeleton overlay. Use when the wrapped child has no 34 34 * radius of its own but a visual descendant does (e.g. wrapping a `TextField`). ··· 54 54 */ 55 55 export function LoadingSkeleton(props: LoadingSkeletonProps): ReactNode { 56 56 const { 57 - as: Component = 'span', 57 + elementType: Component = 'span', 58 58 children, 59 59 className, 60 60 isLoading: isLoadingProp,
+2 -2
packages/@luke-ui/react/src/loading-skeleton/loading-skeleton.stories.tsx
··· 88 88 ), 89 89 }); 90 90 91 - /** Use `as` when parent markup needs an element other than `span`. */ 91 + /** Use `elementType` when parent markup needs an element other than `span`. */ 92 92 export const ElementType = meta.story({ 93 93 play: async ({ canvasElement }) => { 94 94 await expect(canvasElement.querySelector('li[aria-hidden]')).toBeInTheDocument(); 95 95 }, 96 96 render: () => ( 97 97 <ul> 98 - <LoadingSkeleton as="li">List item placeholder</LoadingSkeleton> 98 + <LoadingSkeleton elementType="li">List item placeholder</LoadingSkeleton> 99 99 </ul> 100 100 ), 101 101 });
+14
packages/@luke-ui/react/src/types/render-prop.ts
··· 1 + import type { ComponentPropsWithRef, JSX, ReactElement } from 'react'; 2 + 3 + /** 4 + * Render-delegation prop shared across Luke UI. Mirrors react-aria-components' `render` prop: 5 + * receive the component's resolved DOM props (and optional render state) and return the element to 6 + * render, so a caller can swap the underlying element while keeping the component's props and styles. 7 + * 8 + * `react-aria-components` does not publicly export its equivalent render-function type, so this is the 9 + * canonical Luke UI definition. Reuse it instead of hand-rolling a new render prop per component. 10 + */ 11 + export type RenderProp< 12 + ElementType extends keyof JSX.IntrinsicElements = 'div', 13 + RenderState = undefined, 14 + > = (props: ComponentPropsWithRef<ElementType>, renderState: RenderState) => ReactElement;