Skip to content

Commit cd1eb43

Browse files
committed
Support single slot
1 parent f4c74d6 commit cd1eb43

File tree

2 files changed

+201
-2
lines changed

2 files changed

+201
-2
lines changed

src/codegen/Codegen.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,20 @@ export class Codegen {
354354
)
355355
}
356356

357+
// Handle native Figma SLOT nodes — render as {children} in the component.
358+
// SLOT is a newer Figma node type not yet in @figma/plugin-typings.
359+
if ((node.type as string) === 'SLOT') {
360+
perfEnd('buildTree()', tBuild)
361+
return {
362+
component: 'children',
363+
props: {},
364+
children: [],
365+
nodeType: 'SLOT',
366+
nodeName: node.name,
367+
isSlot: true,
368+
}
369+
}
370+
357371
// Handle asset nodes (images/SVGs)
358372
const assetNode = checkAssetNode(node)
359373
if (assetNode) {
@@ -393,6 +407,20 @@ export class Codegen {
393407
const componentName = getComponentName(mainComponent || node)
394408
const variantProps = extractInstanceVariantProps(node)
395409

410+
// Check for native SLOT children and build their overridden content.
411+
// SLOT children contain the content placed into the component's slot.
412+
const slotChildren: NodeTree[] = []
413+
if ('children' in node) {
414+
for (const child of node.children) {
415+
if ((child.type as string) === 'SLOT' && 'children' in child) {
416+
for (const slotContent of (child as SceneNode & ChildrenMixin)
417+
.children) {
418+
slotChildren.push(await this.buildTree(slotContent))
419+
}
420+
}
421+
}
422+
}
423+
396424
// Only compute position + transform (sync, no Figma API calls)
397425
const posProps = getPositionProps(node)
398426
if (posProps?.pos) {
@@ -417,7 +445,7 @@ export class Codegen {
417445
{
418446
component: componentName,
419447
props: variantProps,
420-
children: [],
448+
children: slotChildren,
421449
nodeType: node.type,
422450
nodeName: node.name,
423451
isComponent: true,
@@ -432,7 +460,7 @@ export class Codegen {
432460
return {
433461
component: componentName,
434462
props: variantProps,
435-
children: [],
463+
children: slotChildren,
436464
nodeType: node.type,
437465
nodeName: node.name,
438466
isComponent: true,
@@ -625,6 +653,16 @@ export class Codegen {
625653
}
626654
const variantComments = selectorProps?.variantComments || {}
627655

656+
// Detect native SLOT children — add children: React.ReactNode to variants
657+
if (
658+
!variants.children &&
659+
childrenTrees.some(
660+
(child) => child.nodeType === 'SLOT' && child.component === 'children',
661+
)
662+
) {
663+
variants.children = 'React.ReactNode'
664+
}
665+
628666
this.componentTrees.set(nodeId, {
629667
name: getComponentName(node),
630668
node,

src/codegen/__tests__/codegen.test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3456,6 +3456,84 @@ describe('Codegen Tree Methods', () => {
34563456
expect(tree.children.length).toBe(1)
34573457
expect(tree.children[0].isComponent).toBe(true)
34583458
})
3459+
3460+
test('builds tree for native SLOT node', async () => {
3461+
const slotContent = {
3462+
type: 'FRAME',
3463+
name: 'DefaultContent',
3464+
children: [],
3465+
visible: true,
3466+
strokes: [],
3467+
effects: [],
3468+
reactions: [],
3469+
} as unknown as FrameNode
3470+
3471+
const node = {
3472+
type: 'SLOT',
3473+
name: 'ContentSlot',
3474+
children: [slotContent],
3475+
visible: true,
3476+
strokes: [],
3477+
effects: [],
3478+
fills: [],
3479+
} as unknown as SceneNode
3480+
addParent(node)
3481+
3482+
const codegen = new Codegen(node)
3483+
const tree = await codegen.buildTree()
3484+
3485+
expect(tree.component).toBe('children')
3486+
expect(tree.nodeType).toBe('SLOT')
3487+
expect(tree.isSlot).toBe(true)
3488+
expect(tree.children).toEqual([])
3489+
})
3490+
3491+
test('builds INSTANCE tree with native SLOT children content', async () => {
3492+
const slotContent = {
3493+
type: 'FRAME',
3494+
name: 'UserContent',
3495+
children: [],
3496+
visible: true,
3497+
strokes: [],
3498+
effects: [],
3499+
reactions: [],
3500+
} as unknown as FrameNode
3501+
3502+
const slotNode = {
3503+
type: 'SLOT',
3504+
name: 'ContentSlot',
3505+
children: [slotContent],
3506+
visible: true,
3507+
strokes: [],
3508+
effects: [],
3509+
fills: [],
3510+
} as unknown as SceneNode
3511+
3512+
const node = {
3513+
type: 'INSTANCE',
3514+
name: 'BottomSheet',
3515+
children: [slotNode],
3516+
visible: true,
3517+
componentProperties: {},
3518+
getMainComponentAsync: async () =>
3519+
({
3520+
type: 'COMPONENT',
3521+
name: 'BottomSheet',
3522+
children: [],
3523+
visible: true,
3524+
}) as unknown as ComponentNode,
3525+
} as unknown as InstanceNode
3526+
addParent(node)
3527+
3528+
const codegen = new Codegen(node)
3529+
const tree = await codegen.buildTree()
3530+
3531+
expect(tree.component).toBe('BottomSheet')
3532+
expect(tree.isComponent).toBe(true)
3533+
// SLOT content should be extracted as children
3534+
expect(tree.children.length).toBe(1)
3535+
expect(tree.children[0].nodeName).toBe('UserContent')
3536+
})
34593537
})
34603538

34613539
describe('getTree', () => {
@@ -3997,6 +4075,67 @@ describe('Codegen Tree Methods', () => {
39974075
expect(textTree).toBeDefined()
39984076
expect(textTree?.textChildren).toEqual(['{children}'])
39994077
})
4078+
4079+
test('detects native SLOT children and adds children: React.ReactNode to variants', async () => {
4080+
const textChild = {
4081+
type: 'TEXT',
4082+
name: 'Title',
4083+
visible: true,
4084+
characters: 'Hello',
4085+
getStyledTextSegments: () => [createTextSegment('Hello')],
4086+
strokes: [],
4087+
effects: [],
4088+
reactions: [],
4089+
textAutoResize: 'WIDTH_AND_HEIGHT',
4090+
} as unknown as TextNode
4091+
4092+
const slotChild = {
4093+
type: 'SLOT',
4094+
name: 'ContentSlot',
4095+
children: [],
4096+
visible: true,
4097+
strokes: [],
4098+
effects: [],
4099+
fills: [],
4100+
} as unknown as SceneNode
4101+
4102+
const defaultVariant = {
4103+
type: 'COMPONENT',
4104+
name: 'State=Default',
4105+
children: [textChild, slotChild],
4106+
visible: true,
4107+
reactions: [],
4108+
} as unknown as ComponentNode
4109+
4110+
const node = {
4111+
type: 'COMPONENT_SET',
4112+
name: 'BottomSheet',
4113+
children: [defaultVariant],
4114+
defaultVariant,
4115+
visible: true,
4116+
componentPropertyDefinitions: {},
4117+
} as unknown as ComponentSetNode
4118+
addParent(node)
4119+
4120+
const codegen = new Codegen(node)
4121+
await codegen.buildTree()
4122+
4123+
const componentTrees = codegen.getComponentTrees()
4124+
const compTree = [...componentTrees.values()].find(
4125+
(ct) => ct.name === 'BottomSheet',
4126+
)
4127+
expect(compTree).toBeDefined()
4128+
4129+
// Should have native SLOT as {children} placeholder
4130+
const slotTree = compTree?.tree.children.find(
4131+
(c) => c.isSlot && c.component === 'children',
4132+
)
4133+
expect(slotTree).toBeDefined()
4134+
expect(slotTree?.nodeType).toBe('SLOT')
4135+
4136+
// Should add children: React.ReactNode to variants
4137+
expect(compTree?.variants.children).toBe('React.ReactNode')
4138+
})
40004139
})
40014140

40024141
describe('renderTree (static)', () => {
@@ -4142,6 +4281,28 @@ describe('Codegen Tree Methods', () => {
41424281
const result = Codegen.renderTree(tree)
41434282
expect(result).toBe('{showBox && <Box />}')
41444283
})
4284+
4285+
test('renders native SLOT as {children}', () => {
4286+
const tree = {
4287+
component: 'Flex',
4288+
props: {},
4289+
children: [
4290+
{
4291+
component: 'children',
4292+
props: {},
4293+
children: [],
4294+
nodeType: 'SLOT',
4295+
nodeName: 'ContentSlot',
4296+
isSlot: true,
4297+
},
4298+
],
4299+
nodeType: 'FRAME',
4300+
nodeName: 'Parent',
4301+
}
4302+
4303+
const result = Codegen.renderTree(tree)
4304+
expect(result).toContain('{children}')
4305+
})
41454306
})
41464307

41474308
describe('INSTANCE_SWAP / BOOLEAN snapshot', () => {

0 commit comments

Comments
 (0)