Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First stab at custom elements manifest #83

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

DmitrySharabin
Copy link
Member

@DmitrySharabin DmitrySharabin commented Jun 4, 2024

At the moment, we need to manually write docs for color elements (reference tables for slots, parts, props, events, etc.), even though most of the info can be inferred from their source code or JSDocs.

There is a package called @custom-elements-manifest/analyzer that allows us to build a custom element manifest file from the custom element source code. We can use the information in this file to automate the generation of docs (in some way).

But there are some steps we need to perform to get the most out of the mentioned package:

  1. Update JSDoc for a color element's class:
    • Describe every slot with the @slot tag
    • Describe every part with the @part tag
    • Describe every custom CSS property with the @cssproperty tag
/**
 * A color picker element.
 *
 * @slot - The color picker's main content. Goes into the swatch.
 * @slot swatch - An element used to provide a visual preview of the current color.
 *
 * @csspart swatch - The default `<color-swatch>` element, used if the `swatch` slot has no slotted elements.
 *
 * @cssproperty {<color>} --slider-thumb-background - Background color of the slider thumb.
 */
export default class ColorPicker extends NudeElement {}
  1. Add JSDocs to the props we'd like to expose (and include in the docs)
    • Main content becomes the prop description
    • Type can be specified with the @type tag, or it will be inferred from the prop's spec (for simple cases; for more complex ones, it's much easier to provide a @type tag)
    • Default value can be specified with the @default tag, or it will be inferred from the prop's spec (unless default is a function)
    • The reflect property is also (mostly—see note below) supported, so we automatically get the corresponding attributes from the spec.
static props = {
	/**
	 * The color space to use for interpolation.
	 * @type {ColorSpace | string}
	 */
	space: {
		default: "oklch",
		parse (value) {},
		stringify (value) {},
	},

	defaultColor: {
		type: Color,
		convert (color) {},
		default () {},
		reflect: {
			from: "color",
		},
	},

	/**
	 * The current color value.
	 * @default oklch(70% 0.25 138)
	 */
	color: {
		type: Color,
		set (value) {},
	},
};
  1. Add JSDocs for every event in the events property:
    • Main content becomes the event description
    • Optional @type attribute can be used to specify the event's type
static events = {
	/**
	 * Fired when the color changes due to user action, either with the sliders or the color swatch's input field.
	 */
	change: {
		from () {},
	},

	/**
	 * Fired when the color changes due to user action, either with the sliders or the color swatch's input field.
	 */
	input: {
		from () {},
	},

	/**
	 * Fired when the color changes for any reason, and once during initialization.
	 * @type {PropChangeEvent}
	 */
	colorchange: {
		propchange: "color",
	},
};

Unfortunately, the @custom-elements-manifest/analyzer package supports only the first step out of the box. For example, it doesn't consider JSDocs for object properties (so there is no way to document events and props). However, the package supports plugins, and I wrote a couple to support the features we need. Under the hood, it analyzes the AST built from a color element's source code by the TypeScript compiler (provided by the package).

The corresponding manifest file might look like this (for <color-picker> for brevity):

{
  "schemaVersion": "1.0.0",
  "readme": "",
  "modules": [
    {
      "kind": "javascript-module",
      "path": "src/color-picker/color-picker.js",
      "declarations": [
        {
          "kind": "class",
          "description": "A color picker element.",
          "name": "ColorPicker",
          "cssProperties": [
            {
              "type": {
                "text": "<color>"
              },
              "description": "Background color of the slider thumb.",
              "name": "--slider-thumb-background"
            }
          ],
          "cssParts": [
            {
              "description": "The default `<color-swatch>` element, used if the `swatch` slot has no slotted elements.",
              "name": "swatch"
            }
          ],
          "slots": [
            {
              "description": "The color picker's main content. Goes into the swatch.",
              "name": ""
            },
            {
              "description": "An element used to provide a visual preview of the current color.",
              "name": "swatch"
            }
          ],
          "members": [
            {
              "kind": "field",
              "name": "tagName",
              "type": {
                "text": "string"
              },
              "static": true,
              "default": "\"color-picker\""
            },
            {
              "kind": "field",
              "name": "Color",
              "static": true,
              "default": "Color"
            },
            {
              "kind": "field",
              "name": "space",
              "description": "The color space to use for interpolation.",
              "type": {
                "text": "ColorSpace | string"
              },
              "default": "oklch",
              "reflects": true,
              "attribute": "space"
            },
            {
              "kind": "field",
              "name": "color",
              "description": "The current color value.",
              "type": {
                "text": "Color"
              },
              "default": "oklch(70% 0.25 138)",
              "reflects": true,
              "attribute": "color"
            }
          ],
          "events": [
            {
              "name": "change",
              "description": "Fired when the color changes due to user action, either with the sliders or the color swatch's input field."
            },
            {
              "name": "input",
              "description": "Fired when the color changes due to user action, either with the sliders or the color swatch's input field."
            },
            {
              "name": "colorchange",
              "description": "Fired when the color changes for any reason, and once during initialization.",
              "type": {
                "text": "PropChangeEvent"
              }
            }
          ],
          "superclass": {
            "name": "NudeElement",
            "module": "/node_modules/nude-element/src/Element.js"
          },
          "tagName": "color-picker",
          "attributes": [
            {
              "name": "space",
              "type": {
                "text": "string"
              },
              "fieldName": "space",
              "default": "oklch"
            },
            {
              "name": "color",
              "type": {
                "text": "string"
              },
              "fieldName": "color",
              "default": "oklch(70% 0.25 138)"
            }
          ],
          "customElement": true
        }
      ],
      "exports": [
        {
          "kind": "js",
          "name": "default",
          "declaration": {
            "name": "ColorPicker",
            "module": "src/color-picker/color-picker.js"
          }
        },
        {
          "kind": "custom-element-definition",
          "declaration": {
            "name": "ColorPicker",
            "module": "src/color-picker/color-picker.js"
          }
        }
      ]
    }
  ]
}

As you can see, it includes all the info we need to generate docs. Getters are also be there: in the manifest file, they are class members with the readonly property:

{
  "kind": "field",
  "name": "foo",
  "readonly": true
}

Issues

There is one more thing to consider. All our color elements now export the Self constant instead of the class itself. I get what benefits we have from this. However, tools like @custom-elements-manifest/analyzer or TypeDoc don't go beyond the default export and don't see the class itself. As a result, we face issues like this one: nudeui/element#12. In our case, we might end up with the following manifest file (for <color-slider> for brevity):

{
  "schemaVersion": "1.0.0",
  "readme": "",
  "modules": [
    {
      "kind": "javascript-module",
      "path": "src/color-slider/color-slider.js",
      "declarations": [
        {
          "kind": "variable",
          "name": "Self",
          "default": "class ColorSlider extends NudeElement { static postConstruct = []; ... static formAssociated = { ... }; }"
        }
      ],
      "exports": [
        {
          "kind": "custom-element-definition",
          "declaration": {
            "name": "Self",
            "module": "src/color-slider/color-slider.js"
          }
        },
        {
          "kind": "js",
          "name": "default",
          "declaration": {
            "name": "Self",
            "module": "src/color-slider/color-slider.js"
          }
        }
      ]
    }
  ]
}

This is definitely not what we want. So, I wonder if there are any strong objections to switching back to export default class ... instead of using Self.

ToDos

  • Add and configure the tool
  • Write a script to generate corresponding reference tables for color elements docs from the manifest file
  • Add JSDocs to color elements

Right now, in this (draft) PR, I addressed the first part of docs generation—I added and configured the tool to build the (correct) manifest file for our color elements.

Notes

reflect is supported mainly because it doesn't support the reflect.to case, and I'm still unsure how to distinguish attributes and properties with one directional reflection in the manifest file. I will research it more.

Copy link

netlify bot commented Jun 4, 2024

Deploy Preview for color-elements ready!

Name Link
🔨 Latest commit dafcb65
🔍 Latest deploy log https://app.netlify.com/sites/color-elements/deploys/665f42ef6d21f10008963f37
😎 Deploy Preview https://deploy-preview-83--color-elements.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@LeaVerou
Copy link
Member

I wonder if instead of jumping through all these hoops it may be easier to DIY it. A lot of the stuff that we need to expose in the CEM is already declarative, all we'd need to do is read each component and extract the relevant definitions. We wouldn't have descriptions, but perhaps we can deal with that later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants