color-js / elements

WIP
14 stars 1 forks source link

First stab at custom elements manifest #83

Open DmitrySharabin opened 5 months ago

DmitrySharabin commented 5 months ago

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: https://github.com/nudeui/element/issues/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

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.

netlify[bot] commented 5 months ago

Deploy Preview for color-elements ready!

Name Link
Latest commit dafcb659b0745a18fedb6a9bb36f5c539677b929
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 commented 1 day ago

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.