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!: exclude clutter from the component API #167

Merged
merged 15 commits into from
Dec 11, 2023
Merged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"validate": "concurrently npm:validate:*",
"validate:types": "tsc --noEmit",
"validate:prettier": "prettier -c scripts src test types karma.config.cjs vite.config.ts",
"validate:build": "tsx scripts/validate-build.ts",
"version": "tsx scripts/package-json-version.ts && npm install --package-lock-only --ignore-scripts"
},
"repository": {
Expand Down
35 changes: 35 additions & 0 deletions scripts/validate-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { readFile } from 'node:fs/promises';

const root = new URL('../', import.meta.url);
const generated = new URL('generated/', root);

/**
* This function validates that the generated TypeScript definition file for a component
* does not include properties that are inherited from a parent class or interface.
*
* It reads the content of the component's generated d.ts file, and checks for the presence of
* certain properties that are expected to be inherited.
*
* If any of these properties are found in the file, the function throws an error.
*
* @throws {Error} If the generated file contains any of the inherited properties.
*/
async function validateInheritedProperties() {
const typeDefinitionContent = await readFile(new URL('Checkbox.d.ts', generated), 'utf8');

// Expect the generated file not to contain definitions for properties that are inherited.
const inheritedProperties = [
'className', // From 'HTMLAttributes'
'onClick', // From 'DOMAttributes'
'theme', // From 'ThemePropertyMixinClass'
'indeterminate', // From 'CheckboxMixin'
];

for (const property of inheritedProperties) {
if (typeDefinitionContent.includes(`${property}:`) || typeDefinitionContent.includes(`${property}?:`)) {
throw new Error(`The generated file contains the extracted property "${property}".`);
}
}
}

await Promise.all([validateInheritedProperties()]);
12 changes: 11 additions & 1 deletion src/utils/createComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import type { ThemePropertyMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
import type React from 'react';
import type { RefAttributes } from 'react';
import type { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';

declare const __VERSION__: string;

Expand Down Expand Up @@ -59,15 +60,24 @@ export type ThemedWebComponentProps<
theme?: string;
};

export type WebComponentProps<I extends HTMLElement, E extends EventNames = {}> = I extends ThemePropertyMixinClass
type AllWebComponentProps<I extends HTMLElement, E extends EventNames = {}> = I extends ThemePropertyMixinClass
? ThemedWebComponentProps<I, E>
: _WebComponentProps<I, E>;

// TODO: LoginOverlay has "autofocus" property so we can't omit it
type OmittedWebComponentProps = Omit<HTMLElement, keyof React.HTMLAttributes<any> | 'autofocus'> & ControllerMixinClass;

export type WebComponentProps<I extends HTMLElement, E extends EventNames = {}> = Omit<
AllWebComponentProps<I, E>,
keyof OmittedWebComponentProps
>;

// We need a separate declaration here; otherwise, the TypeScript fails into the
// endless loop trying to resolve the typings.
export function createComponent<I extends HTMLElement, E extends EventNames = {}>(
options: Options<I, E>,
): (props: WebComponentProps<I, E> & RefAttributes<I>) => React.ReactElement | null;

export function createComponent<I extends HTMLElement, E extends EventNames = {}>(options: Options<I, E>): any {
const { elementClass } = options;

Expand Down
100 changes: 100 additions & 0 deletions test/typings/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { type HTMLAttributes, type RefAttributes } from 'react';
import { TextField, TextFieldElement } from '../../TextField.js';
import type { LitElement } from 'lit';
import { GridColumn, GridColumnElement } from '../../GridColumn.js';
import { Dialog, DialogElement } from '../../Dialog.js';
import { DatePicker, DatePickerElement } from '../../DatePicker.js';
import { LoginOverlay, LoginOverlayElement } from '../../LoginOverlay.js';
import { TimePicker, type TimePickerChangeEvent } from '../../TimePicker.js';
import { TextArea, TextAreaElement, type TextAreaChangeEvent } from '../../TextArea.js';
import { MessageInput, MessageInputElement, type MessageInputSubmitEvent } from '../../MessageInput.js';
import { ComboBox, type ComboBoxChangeEvent } from '../../ComboBox.js';

const assertType = <TExpected>(value: TExpected) => value;
const assertOmitted = <C, T>(prop: keyof Omit<C, keyof T>) => prop;

const textFieldProps = React.createElement(TextField, {}).props;
type TextFieldProps = typeof textFieldProps;

type PartialTextFieldElement = Omit<
Partial<TextFieldElement>,
'draggable' | 'style' | 'translate' | 'children' | 'contentEditable'
>;

assertType<PartialTextFieldElement>(textFieldProps);

// Assert that certain properties are present
assertType<PartialTextFieldElement['label']>(textFieldProps.label);
assertType<PartialTextFieldElement['value']>(textFieldProps.value);
assertType<PartialTextFieldElement['hidden']>(textFieldProps.hidden);
assertType<PartialTextFieldElement['slot']>(textFieldProps.slot);
assertType<PartialTextFieldElement['title']>(textFieldProps.title);
assertType<PartialTextFieldElement['id']>(textFieldProps.id);

assertType<HTMLAttributes<TextFieldElement>['className']>(textFieldProps.className);
assertType<HTMLAttributes<TextFieldElement>['style']>(textFieldProps.style);
assertType<HTMLAttributes<TextFieldElement>['children']>(textFieldProps.children);
assertType<HTMLAttributes<TextFieldElement>['aria-label']>(textFieldProps['aria-label']);

assertType<RefAttributes<TextFieldElement>['ref']>(textFieldProps.ref);

// Assert that certain HTMLElement properties are NOT present
assertOmitted<HTMLElement, TextFieldProps>('append');
assertOmitted<HTMLElement, TextFieldProps>('prepend');
assertOmitted<HTMLElement, TextFieldProps>('ariaLabel');

// Assert that certain LitElement properties are NOT present
assertOmitted<LitElement, TextFieldProps>('renderRoot');
assertOmitted<LitElement, TextFieldProps>('requestUpdate');
assertOmitted<LitElement, TextFieldProps>('addController');
assertOmitted<LitElement, TextFieldProps>('removeController');

const gridColumnProps = React.createElement(GridColumn, {}).props;
type GridColumnProps = typeof gridColumnProps;
assertType<GridColumnElement['hidden'] | undefined>(gridColumnProps.hidden);

assertOmitted<HTMLElement, GridColumnProps>('append');
assertOmitted<HTMLElement, GridColumnProps>('prepend');
assertOmitted<HTMLElement, GridColumnProps>('ariaLabel');

const dialogProps = React.createElement(Dialog, {}).props;
type DialogProps = typeof dialogProps;

assertType<DialogElement['ariaLabel'] | undefined>(dialogProps['aria-label']);

assertType<DialogProps['footer']>(dialogProps.footer);

const datePickerProps = React.createElement(DatePicker, {}).props;
type DatePickerProps = typeof datePickerProps;
web-padawan marked this conversation as resolved.
Show resolved Hide resolved

const datePickerOnChange: typeof datePickerProps.onChange = (event) => {
assertType<DatePickerElement['value']>(event.target.value);
};

const comboBoxProps = React.createElement(ComboBox, {}).props;

assertType<typeof comboBoxProps.onChange>((_event: ComboBoxChangeEvent<any>) => {});

const messageInputProps = React.createElement(MessageInput, {}).props;

const messageInputOnSubmit: typeof messageInputProps.onSubmit = (event) => {
assertType<MessageInputElement['value']>(event.detail.value);
};

assertType<typeof messageInputProps.onSubmit>((_event: MessageInputSubmitEvent) => {});

const textAreaProps = React.createElement(TextArea, {}).props;

assertType<typeof textAreaProps.onChange>((_event: TextAreaChangeEvent) => {});

const textAreaOnChange: typeof textAreaProps.onChange = (event) => {
assertType<TextAreaElement['value']>(event.target.value);
};

const timePickerProps = React.createElement(TimePicker, {}).props;

assertType<typeof timePickerProps.onChange>((_event: TimePickerChangeEvent) => {});

const loginOverlayProps = React.createElement(LoginOverlay, {}).props;

assertType<LoginOverlayElement['autofocus'] | undefined>(loginOverlayProps.autofocus);