carbon-design-system / carbon-components-vue

Vue implementation of the Carbon Design System
http://vue.carbondesignsystem.com
Apache License 2.0
608 stars 180 forks source link

Carbon v11 #1587

Open benceszenassy opened 7 months ago

benceszenassy commented 7 months ago

Is your feature request related to a problem? Please describe. We should keep up, with the design kit, react and angular components.

Describe the solution you'd like On a separate branch, we should update the components incrementally, based on the react components.

Additional context There are lot of basic features missing from v10 like readonly form fields, toasts that remove them selves after x ms, etc.

@davidnixon I'm gladly participating in this update, if we can do it.

davidnixon commented 7 months ago

1596

LMK what you think of wrapping web components. The HUGE advantage is that we have a large pool of maintainers on the web components team.

@jeffchew I wrapped cds-button and cds-text-input. Those work pretty well. I would still like to try and generate wrappers automatically so I'll keep exploring that.

davidnixon commented 7 months ago

https://github.com/carbon-design-system/carbon-components-vue/tree/carbon11?tab=readme-ov-file#carbonvue-3

benceszenassy commented 7 months ago

I didn't play too much with web components, the first thing that pops in my mind, is scoped slots, but i think we can workaround it with provide / inject.

There are also some areas where we find custom elements to be limiting: Eager slot evaluation hinders component composition. Vue's scoped slots are a powerful mechanism for component composition, which can't be supported by custom elements due to native slots' eager nature. Eager slots also mean the receiving component cannot control when or whether to render a piece of slot content.
Shipping custom elements with shadow DOM scoped CSS today requires embedding the CSS inside JavaScript so that they can be injected into shadow roots at runtime. They also result in duplicated styles in markup in SSR scenarios. There are platform features being worked on in this area - but as of now they are not yet universally supported, and there are still production performance / SSR concerns to be addressed. In the meanwhile, Vue SFCs provide CSS scoping mechanisms that support extracting the styles into plain CSS files.

vuejs.org

Other than that i think its a good direction.

I ran through your commits, i like the idea of using vite. It now seems like a full rewrite so i would introduce typescript too, i dont think it will be too much of a burden.

For wrapper generation, lit labs have something like that, but i didn't tried it yet - gen-wrapper-vue

benceszenassy commented 6 months ago

Hey @davidnixon, i played a little with this lib, its not near an automated wrapper generator, but it can definitely can generate types for props and such.

I checked out the carbon repository, and wired-in the lib.

package.json

...
  "scripts": {
    "generate-vue-types": "custom-elements-manifest analyze && node generate-vue-types.js",
...
  "devDependencies": {
    "@custom-elements-manifest/analyzer": "^0.10.2",
    "custom-element-vuejs-integration": "^1.2.0",
...

generate-vue-types.js

import { generateVuejsTypes } from 'custom-element-vuejs-integration';
import manifest from './custom-elements.json' assert { type: 'json' };

const options = {
  outdir: './',
  fileName: 'vue-types.d.ts',
  componentTypePath: (name, tag) => `./src/components/${tag}/index.ts`,
};

generateVuejsTypes(manifest, options);

Generated custom-elements.json: custom-elements.json

Generated vue-types.d.ts (d.ts upload not supported): vue-types.txt

Its not exporting its types, but i think for a starter its better than nothing. There are libs for VSCode and JetBrains custom html and css json generations too.

And there is a lib for react component generation, maybe it can help to create a vue component generator? custom-element-react-wrappers

github-actions[bot] commented 5 months ago

This issue has been marked as stale because it has required additional info or a response from the author for over 14 days. When you get the chance, please comment with the additional info requested. Otherwise, this issue will be closed in 14 days.

davidnixon commented 5 months ago

@benceszenassy I have also been looking at tools and I found https://github.com/open-wc/custom-elements-manifest which I think is the same tool you referece above but a different git repo? And this one https://www.npmjs.com/package/custom-element-jet-brains-integration (I use WebStorm).

So I was able to generate this json carbon-web-components-web-types.json

And from that I generated a VERY simple component for the copy button:

<template>
  <cds-copy-button v-bind="props">
    <slot> Copy to Clipboard </slot>
  </cds-copy-button>
</template>
<script setup>
import '@carbon/web-components/es/components/copy-button/index.js';
import { useSlots } from 'vue';
const props = defineProps({
  /** Specify an optional className to be added to your Button */
  buttonClassName: { type: String },
  /** `true` if the button should be disabled. */
  disabled: { type: Boolean },
  /** Specify the string that is displayed when the button is clicked and the content is copi */
  feedback: { type: String, default: 'Copied!' },
  /** The number in milliseconds to determine how long the tooltip should remain. */
  feedbackTimeout: { type: Number, default: 2000 },
  /** Focuses on the first focusable element in the shadow DOM. */
  focus: { type: String },
});
</script>

The generateReactWrappers is VERY interesting. I failed for some of the dot com components but it worked enough to look promising. I'll look at it this week.

I also looked at this one which did not really work at all https://www.npmjs.com/package/@lit-labs/gen-wrapper-vue

davidnixon commented 5 months ago

I think the comment above will remove the stale label when the action rus again.

benceszenassy commented 5 months ago

@davidnixon i thinkered with it too, here is a gist with the generator (its not complete, but it can generate the basics) and with one example of a generated component.

https://gist.github.com/benceszenassy/7c06de0043e2dfe937008477489d24eb

With emits i dont know what would be the best practice, they didnt get any type for payload... we should create x amount of v-on handler with any as payload type, and simply emit with each? There are custom types in some of the props, those cant be generated, the custom-elements.json cant pick up it well.

github-actions[bot] commented 4 months ago

This issue has been marked as stale because it has required additional info or a response from the author for over 14 days. When you get the chance, please comment with the additional info requested. Otherwise, this issue will be closed in 14 days.

benceszenassy commented 3 months ago

@davidnixon I dont have much time to play with the generator, it require more time to build one of these, than convert every component by hand imho.

What will be the vision for vue components? It should follow the react components functionality, or the web components? Should built from scracth or use the web components? If it is from web components what should reactivity be like? Or emitted event typing?

Im more comfortable with vue from scratch than built a web component wrapper library, but i still want to use this library in work, so if you can tell me the way, i can contribute to it.

benceszenassy commented 2 months ago

@davidnixon any update on this?

davidnixon commented 2 months ago

@benceszenassy Yes, I mostly looked at the JetBrains stuff which was helpful but incomplete. It looks like you found the same. I got stuck on not being able to fully generate a component but being able to generate a skeleton of a component. I think you found the same?

Current sticking points for we were the slots. These were not behaving correctly at all for me.

I am still most interested in wrapping the web components:

benceszenassy commented 2 months ago

@davidnixon i had time on my hands today, so a looked into it - these are examples from the generator i played locally (https://github.com/benceszenassy/cem-tools/tree/main | https://github.com/benceszenassy/carbon-components-vue/tree/carbon11)

slots

custom-elements doesn't have scoped slots, so i think its safe to say, that the geneator can handle the slot generation.

<template>
  <cds-textarea class="cv-textarea" v-bind="props">
    <template v-slot:helper-text>
      <slot name="helper-text" />
    </template>
    <template v-slot:label-text>
      <slot name="label-text" />
    </template>
    <template v-slot:validity-message>
      <slot name="validity-message" />
    </template>

    <slot></slot>
  </cds-textarea>
</template>

<script setup lang="ts">
import "@carbon/web-components/es/components/textarea/textarea.js";
import type { CvTextareaProps } from "./CvTextarea.ts";
const props = defineProps<CvTextareaProps>();

const emits = defineEmits<{
  // undefined
  (e: "invalid", value: CustomEvent): void;
}>();
const slots = defineSlots<{
  // The helper text.
  "helper-text": (scope: any) => any;
  // The label text.
  "label-text": (scope: any) => any;
  // The validity message. If present and non-empty, this input shows the UI of its invalid state.
  "validity-message": (scope: any) => any;
}>();
</script>

Or we can add scope, just for our own slot handling.

<template>
  <cds-textarea class="cv-textarea" v-bind="props">
    <template #[:helper-text]="scope">
      <slot name="helper-text" v-bind="scope" />
    </template>
    <template #[:label-text]="scope">
      <slot name="label-text" v-bind="scope" />
    </template>
    <template #[:validity-message]="scope">
      <slot name="validity-message" v-bind="scope" />
    </template>

    <slot></slot>
  </cds-textarea>
</template>

<script setup lang="ts">
import "@carbon/web-components/es/components/textarea/textarea.js";
import type { CvTextareaProps } from "./CvTextarea.ts";
const props = defineProps<CvTextareaProps>();

const emits = defineEmits<{
  // undefined
  (e: "invalid", value: CustomEvent): void;
}>();
const slots = defineSlots<{
  // The helper text.
  "helper-text": (scope: any) => any;
  // The label text.
  "label-text": (scope: any) => any;
  // The validity message. If present and non-empty, this input shows the UI of its invalid state.
  "validity-message": (scope: any) => any;
}>();
</script>

Or use a single runtime cycle.

<template>
  <cds-textarea class="cv-textarea" v-bind="props">
    <template v-for="(_, slot) of $slots" #[slot]="scope">
      <slot :name="slot" v-bind="scope" />
    </template>
  </cds-textarea>
</template>

<script setup lang="ts">
import "@carbon/web-components/es/components/textarea/textarea.js";
import type { CvTextareaProps } from "./CvTextarea.ts";
const props = defineProps<CvTextareaProps>();

const emits = defineEmits<{
  // undefined
  (e: "invalid", value: CustomEvent): void;
}>();
const slots = defineSlots<{
  // The helper text.
  "helper-text": (scope: any) => any;
  // The label text.
  "label-text": (scope: any) => any;
  // The validity message. If present and non-empty, this input shows the UI of its invalid state.
  "validity-message": (scope: any) => any;
}>();
</script>

emits

For example the CDSTabs use events from CDSContentSwitcher through a mixin (HostListenerMixin), i think the type data lost on the generic class generation, but if we had the information, in this example, the data that travels with the event is a reference for an HTML element, we can only extract it (blindly without types), and emit it further.

// content-switcher.ts
  /**
   * Handles `click` event on content switcher item.
   *
   * @param event The event.
   * @param event.target The event target.
   */
  protected _handleClick({ target }: MouseEvent) {
    const currentItem = this._getCurrentItem(target as HTMLElement);
    this._handleUserInitiatedSelectItem(currentItem as CDSContentSwitcherItem);
  }

  /**
   * Handles user-initiated selection of a content switcher item.
   *
   * @param [item] The content switcher item user wants to select.
   */
  protected _handleUserInitiatedSelectItem(item: CDSContentSwitcherItem) {
    if (!item.disabled && item.value !== this.value) {
      const init = {
        bubbles: true,
        composed: true,
        detail: {
          item,
        },
      };
      const constructor = this.constructor as typeof CDSContentSwitcher;
      const beforeSelectEvent = new CustomEvent(constructor.eventBeforeSelect, {
        ...init,
        cancelable: true,
      });
      if (this.dispatchEvent(beforeSelectEvent)) {
        this._selectionDidChange(item);
        const afterSelectEvent = new CustomEvent(constructor.eventSelect, init);
        this.dispatchEvent(afterSelectEvent);
      }
    }
  }

  /**
   * The name of the custom event fired after a a content switcher item is selected upon a user gesture.
   */
  static get eventSelect() {
    return `${prefix}-content-switcher-selected`;
  }
// tabs.ts
  /**
   * The name of the custom event fired after a a tab is selected upon a user gesture.
   */
  static get eventSelect() {
    return `${prefix}-tabs-selected`;
  }
<!-- CvTabs.vue -->
<template>
  <cds-tabs
    class="cv-tabs"
    v-bind="props"
    @cds-tabs-selected="e => emits('cds-tabs-selected', e.detail.item.value)"
  >
    <slot></slot>
  </cds-tabs>
</template>

<script setup lang="ts">
import '@carbon/web-components/es/components/tabs/tabs';
import type { CvTabsProps } from './CvTabs.ts';
const props = defineProps<CvTabsProps>();

const emits = defineEmits<{
  // The custom event fired before a tab is selected upon a user gesture. Cancellation of this event stops changing the user-initiated selection.
  (e: 'cds-tabs-beingselected', value: undefined): void;
  // The custom event fired after a a tab is selected upon a user gesture.
  (e: 'cds-tabs-selected', value: undefined): void;
  // The custom event fired before a content switcher item is selected upon a user gesture. Cancellation of this event stops changing the user-initiated selection.
  (e: 'cds-content-switcher-beingselected', value: undefined): void;
  // The custom event fired after a a content switcher item is selected upon a user gesture.
  (e: 'cds-content-switcher-selected', value: undefined): void;
}>();
</script>

v-model

With the previous example we can only define / use v-model by-hand.

<template>
  <cds-tabs class="cv-tabs" v-bind="props" @cds-tabs-selected="onSelect">
    <slot></slot>
  </cds-tabs>
</template>

<script setup lang="ts">
import '@carbon/web-components/es/components/tabs/tabs';
import type { CvTabsProps } from './CvTabs.ts';
const props = defineProps<CvTabsProps>();

const model = defineModel('value');

const onSelect = e => {
  emits('cds-tabs-selected', e.detail.item.value);
  model.value = e.detail.item.value;
};

const emits = defineEmits<{
  // The custom event fired before a tab is selected upon a user gesture. Cancellation of this event stops changing the user-initiated selection.
  (e: 'cds-tabs-beingselected', value: undefined): void;
  // The custom event fired after a a tab is selected upon a user gesture.
  (e: 'cds-tabs-selected', value: undefined): void;
  // The custom event fired before a content switcher item is selected upon a user gesture. Cancellation of this event stops changing the user-initiated selection.
  (e: 'cds-content-switcher-beingselected', value: undefined): void;
  // The custom event fired after a a content switcher item is selected upon a user gesture.
  (e: 'cds-content-switcher-selected', value: undefined): void;
}>();
</script>

props

I think it depends on the custom elements not the vue component it self.

benceszenassy commented 1 month ago

@davidnixon let me know if i can help with anything else on this topic