code-connect icon indicating copy to clipboard operation
code-connect copied to clipboard

figma.children descendents and React Fragments on children

Open ericandrewscott opened this issue 1 year ago • 3 comments

First, I'll start off by saying, I know our design components need a lot of love to bring them up to current Figma capabilities. With that said, I think there are a lot of other large design systems (public or private) that are in the same boat, as updating 50-70 components with breaking design changes is actually a lot of work to coordinate at an enterprise level!

So we're working through hooking up Code Connect, and while it is a MASSIVE improvement over the previous plugin + templating system, there are still a few quirky things, at least as far as our design system is concerned, especially with the way figma.children works. I'll outline our two major issues here, but I've also chimed on related issues I've found on the repo.

Nesting

We have many components that aren't utilizing the setting where layers are hidden on publish. This means that we'll often have a few layers between the top-level component, and the child markup we want to render. An example of this is Button. As you can see with the below screenshot, we have several subcomponents between the actual variant of the button and the two other things we care about rendering in the component (icon + text). Screenshot 2024-09-05 at 8 39 58 AM

In order to render out the child Icon or get the dynamic text, which is regrettably not represented by a label property on the component, we have to use nestedProps and static markup, because figma.children is not recursive.

The end code ends up looking like:

<PrimaryButton size="large" onClick={() => {}}>
  <Icon name={"icon_name_here"} /> Button label
</PrimaryButton>

In this case, we are able to grab the dynamic text string with either figma.string or figma.textContent, but we're not able to retrieve the actual icon, which is in this case, info. However, if figma.children was recursive, we could do:

figma.connect(
  PrimaryButton,
  'https://www.figma.com/design/<OBFUSCATED_URL>,
  {
    variant: { Content: 'Icon leading + Label' },
    icon: { figma.children(['Icon glyph']) },
    props: {
      // these are typically spread from a few constants but including for clarity
      size: figma.enum('Size', {
        Small: ButtonSize.SMALL,
        Large: ButtonSize.LARGE,
      }),
      disabled: figma.enum('State', {
        Disabled: true,
      }),
      fullwidth: figma.boolean('Full-width'),
      label: figma.nestedProps('_ / Button / Label', {
        text: figma.textContent('✏️ Label'),
      }),
      inverse: figma.boolean('Inverse'),
    },
    example: ({ disabled, size, fullwidth, inverse, label, icon }) => (
      <PrimaryButton size={size} disabled={disabled} isFullWidth={fullwidth} inverse={inverse} onClick={() => {}}>
        {icon} {label.text}
      </PrimaryButton>
    ),
  },
);

Additionally, we could even get ride of the variant specification altogether and simplify our number of figma.connects, since we're having to define 3 different connects:

  • one for plain label
  • one for leading icon + label
  • one for label + trailing icon If we could recursively just return {children} and provide optional nested components.

Fragments <></>

Using the same example of Button, if we wanted to hook up the glyph icon component to the return the string needed by the child (why we're using a glyph instead of our Icon component is another story for another day), then we would have to return a React Fragment with the string, though we'd like to omit it. In the .figma.tsx file, that would look like:

figma.connect(
  DummyComponent,
  'https://www.figma.com/design/<OBFUSCATED>',
  {
    imports: [''],
    props: {
      iconString: "info"
    },
    example: ({ icon }) => <>{icon}</>,
  }
);

Then the button code could look like:

figma.connect(
  PrimaryButton,
  'https://www.figma.com/design/<OBFUSCATED_URL>,
  {
    variant: { Content: 'Icon leading + Label' },
    icon: { figma.children(['Icon glyph']) },
    props: {
      // these are typically spread from a few constants but including for clarity
      size: figma.enum('Size', {
        Small: ButtonSize.SMALL,
        Large: ButtonSize.LARGE,
      }),
      disabled: figma.enum('State', {
        Disabled: true,
      }),
      fullwidth: figma.boolean('Full-width'),
      label: figma.nestedProps('_ / Button / Label', {
        text: figma.textContent('✏️ Label'),
      }),
      inverse: figma.boolean('Inverse'),
    },
    example: ({ disabled, size, fullwidth, inverse, label, icon }) => (
      <PrimaryButton size={size} disabled={disabled} isFullWidth={fullwidth} inverse={inverse} onClick={() => {}}>
        <Icon name={icon} /> {label.text}
      </PrimaryButton>
    ),
  },
);

But, currently, that would render:

<PrimaryButton size="large" onClick={() => {}}>
<Icon name={<>info</>} /> Button label
</PrimaryButton>

Any pointers on either of these issues? I'm also happy to dig in and maybe try to contribute to 1.0.7 (or beyond)!

  • Code Connect CLI version 1.0.6
  • OSX

ericandrewscott avatar Sep 05 '24 14:09 ericandrewscott

Hey @ericandrewscott, this is great, thanks for all the detail!

Nesting

If I'm understanding correctly you can achieve this today by using nestedProps. Helpers like children/textContent do not recurse into instances by default, and nestedProps is an escape hatch to do that. So your example might look like:

instanceProps: figma.nestedProps('_ / Button / Icon', {
  children: figma.children("Icon Glyph"),
})

// usage {props.instanceProps.children}

Fragments

A limitation of our API today is all code connect examples return as JSX. Icons in particular this is very problematic for and a top priority for us is working out how we can either allow returning strings, or achieve connecting icons in a different way.

slees-figma avatar Sep 06 '24 10:09 slees-figma

@slees-figma Christopher Hodges told us on Slack that he spoke to the product team specifically about the Fragments bit, and that the web components API might allow us to do this, but to my knowledge, you can't mix two separate implementations. Can you elaborate on this or will there be a future option to obfuscate the <></> from the return?

ericandrewscott avatar Sep 12 '24 15:09 ericandrewscott

Hey @ericandrewscott, we are looking into our options from removing fragments from the returns of code connect (or achieving this in a different way for icons). And that's correct, right now the API does not support mixing and matching frameworks in code connect examples.

slees-figma avatar Sep 16 '24 15:09 slees-figma