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

fix(ui): camelCase to kebab-case (#970) #1013

Merged
merged 1 commit into from
Mar 22, 2024
Merged

Conversation

angsherpa456
Copy link
Contributor

@angsherpa456 angsherpa456 commented Mar 12, 2024

Objective

Ensure that all property names of our custom elements are reflected as kebab-case HTML attributes in the DOM.

Solution: Simple inheritance & LitElement overrides

We created a base class BoilerLitElement that extends LitElement and overrides the static createProperty() method. This custom createProperty() method automatically converts the name of any class members marked with the @property decorator to a kebab-cased attribute in HTML. All our custom elements now extend LitElementCustom instead of LitElement.

This solution is simple, light-weight & works well with TypeScript's class inference features.

Dropped approaches

We investigated other solutions to approach this issue but decided to drop them due to complexity or TypeScript incompatibility.

Class mixin approach (dropped)

Class mixins as proposed in Lit's documentation don't play well with TypeScript currently. They work partially and with workarounds but would introduce a lot of complexity to make them adaptable and future proof for our project. It might still be worth it to investigate the mixin pattern in the future but it is out of scope for this issue.

References:

This is as far as we got during research

import { LitElement, PropertyDeclaration } from 'lit';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Constructor<T> = new (...args: any[]) => T;

export type Constructable<TConstructorResult, TConstructorHost> = Omit<TConstructorHost, never> &
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  Constructor<TConstructorResult>;

// Have to declare an exported class interface to add additional properties to our mixin class.
// Results in non-ignorable error TS4094 otherwise.
export declare class AdditionalMixinProperties {
  static staticMember(): void;
  protected protectedMember(): void;
}

export const camelCaseToKebabCase = (str: string) => str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();

// The mixin factory function
export function kebabCaseAttributes<T extends Constructable<LitElement, typeof LitElement>>(constructor: T) {
  class KebabCased extends constructor {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    constructor(...args: any[]) {
      super(...args);
    }

    static createProperty(name: PropertyKey, options?: PropertyDeclaration<unknown, unknown> | undefined): void {
      let customOptions = options;

      // generate the attribute name if it hasn't been defined or if it's disabled.
      if (typeof options?.attribute === 'undefined' || options?.attribute === true) {
        const attributeName = camelCaseToKebabCase(name.toString());
        customOptions = { ...options, attribute: attributeName };
      }

      return super.createProperty(name, customOptions);
    }
  }

  return KebabCased as Constructable<InstanceType<T> & AdditionalMixinProperties, T & typeof AdditionalMixinProperties>;
}

// Need to create an explicitly named intermediate class to extend from.
// Otherwise: 
// - static member inference won't work on the subclass.
// - Error TS4094 | Property ... of exported class expression may not be private or protected.
// This needs to be done for every class definition that wants to extend any number of mixins.
export class MixinBase extends kebabCaseAttributes(LitElement) {}

// Extend ^ instead of `kebabCaseAttributes(LitElement)` directly
export class CustomElement extends MixinBase {
  // access to iherited protected, private and static members works
}

This solution works on almost all fronts: proper type inference and autocompletion for statics and non-statics in mixin class implementation as well as classes extending the mixin.
It needs more polish to make it easier to adapt for other future mixins. Eg. offer a utility function that creates mixin classes and does the proper type conversion through inference (if possible). It also has the caveat of having to create explicitly named intermediate classes as commented in the code above.

Setting the attribute option in the @property() decorator

We discussed going the route of explicitly naming our attributes through the @property({ attribute: 'some-attribute-name' }) syntax but decided against it since the current solution is succinct and easy to undo should we decide against it in the future.

closes #970

@angsherpa456 angsherpa456 force-pushed the fix/970_kebab-case branch 3 times, most recently from 425be00 to 782d6b3 Compare March 13, 2024 11:06
@@ -0,0 +1,25 @@
import type { PropertyDeclaration } from 'lit-element';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool stuff :)

Copy link
Collaborator

@faselbaum faselbaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the other comments we could think about adding an import restriction rule to our ui-library/.eslintrc.js to prevent ourselves from accidentally using the standard LitElement.

// .eslintrc.js
module.exports = {
  ...
  rules: {
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          {
            group: ['lit'],
            importNamePattern: 'LitElement',
            message: `Don't use the default LitElement class. Import from /utils/boiler-lit-element instead`,
          },
        ],
      },
    ],
  },
  ...
}

packages/ui-library/src/utils/boiler-lit-element.ts Outdated Show resolved Hide resolved
packages/ui-library/src/components/checkbox/index.ts Outdated Show resolved Hide resolved
davidken91
davidken91 previously approved these changes Mar 20, 2024
Copy link

@Jumace Jumace left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a neat solution for the problem of the attributes.

Found three things over all.

  • leftover has-Error instead of hasError
  • the used regEx for creating kebab-case is a bit flawed, posted an alternative solution
  • Somewhere in the documentation it should be noted that only the LitElementCustom should be used

packages/ui-library/src/utils/lit-element-custom.ts Outdated Show resolved Hide resolved
@angsherpa456 angsherpa456 force-pushed the fix/970_kebab-case branch 2 times, most recently from 0858bee to 5679303 Compare March 21, 2024 11:44
@angsherpa456
Copy link
Contributor Author

"Somewhere in the documentation it should be noted that only the LitElementCustom should be used"
Now the information is added in the architecture section.

@angsherpa456 angsherpa456 requested a review from Jumace March 21, 2024 12:06
Copy link
Collaborator

@faselbaum faselbaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we do about properties like these?

They aren't camelCased even though they probably should be. But this might also produce unwanted behaviour with screen readers when kebab-cased. I presume we usually only use them to forward the values to elements further down the tree? We should probably investigate how screen readers behave when aria attributes are duplicated on the custom element and within the shadow dom of said element.

This might lead to screen readers reading the label or whatever twice.

Jumace
Jumace previously approved these changes Mar 21, 2024
davidken91
davidken91 previously approved these changes Mar 21, 2024
@Jumace
Copy link

Jumace commented Mar 21, 2024

For the comment of @faselbaum I'd suggest to cover this in a new ticket. From my understanding should the current implementation not break anything, only if attributes like the arialabel would be written in camelCase it could break.
If you agree with my statement please get in touch with @thrbnhrtmnn to create a new ticket.

@ChristianHoffmannS2
Copy link
Collaborator

ChristianHoffmannS2 commented Mar 21, 2024

What do we do about properties like these?

They aren't camelCased even though they probably should be. But this might also produce unwanted behaviour with screen readers when kebab-cased. I presume we usually only use them to forward the values to elements further down the tree? We should probably investigate how screen readers behave when aria attributes are duplicated on the custom element and within the shadow dom of said element.

This might lead to screen readers reading the label or whatever twice.

a few things here.

  • property names should not match default html attribute names. (thats why it never was camelcased in the first place, it warns us against that)

  • the input field is connected to a real label by id. i dont see a need for an additional arial label. and i am also not sure how accessibility tools would choose between those two label options.

i would ignore it for now, we will definitely stumble on this one during the accessibility epic.

Copy link
Collaborator

@faselbaum faselbaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The js-example-app needs to be updated to reflect the changes as well since we're not using the genericBlrComponentRenderer function there.

@angsherpa456
Copy link
Contributor Author

angsherpa456 commented Mar 22, 2024

The js-example-app needs to be updated to reflect the changes as well since we're not using the genericBlrComponentRenderer function there.

True, some of them are broken currently.

@angsherpa456 angsherpa456 merged commit 00161d6 into develop Mar 22, 2024
4 of 5 checks passed
@angsherpa456 angsherpa456 deleted the fix/970_kebab-case branch March 22, 2024 12:11
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.

Going Kebab-Case All The Way
5 participants