Skip to content

Commit

Permalink
feat: Adds findAllComponents selector
Browse files Browse the repository at this point in the history
  • Loading branch information
orangevolon committed Nov 6, 2024
1 parent f49a5a9 commit 929af94
Show file tree
Hide file tree
Showing 6 changed files with 441 additions and 7 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 43 additions & 3 deletions src/core/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
/*eslint-env browser*/
import { IElementWrapper } from './interfaces';
import { KeyCode, isScopedSelector, substituteScope } from './utils';
import { KeyCode, isScopedSelector, substituteScope, appendSelector } from './utils';
import { act } from './utils-dom';

// Original KeyboardEventInit lacks some properties https://github.com/Microsoft/TypeScript/issues/15228
Expand All @@ -23,6 +23,14 @@ const defaultParams = {
cancelable: true,
};

interface WrapperClass<Wrapper, ElementType> {
new (element: ElementType): Wrapper;
}

interface ComponentWrapperClass<Wrapper, ElementType> extends WrapperClass<Wrapper, ElementType> {
rootSelector: string;
}

export class AbstractWrapper<ElementType extends Element>
implements IElementWrapper<ElementType, Array<ElementWrapper<ElementType>>>
{
Expand Down Expand Up @@ -120,13 +128,45 @@ export class AbstractWrapper<ElementType extends Element>
return this.findAll<NewElementType>(`.${className}`);
}

findComponent<Wrapper extends ComponentWrapper<ElementType>, ElementType extends HTMLElement>(
/**
* Returns the component wrapper matching the specified selector.
* If the specified selector doesn't match any element, it returns `null`.
*
* Note: This function returns the specified component's wrapper even if the specified selector points to a different component type.
*
* @param {string} selector CSS selector
* @param {WrapperClass} ComponentClass Component's wrapper class
* @returns `Wrapper | null`
*/
findComponent<Wrapper extends ComponentWrapper, ElementType extends HTMLElement>(
selector: string,
ComponentClass: { new (element: ElementType): Wrapper }
ComponentClass: WrapperClass<Wrapper, ElementType>
): Wrapper | null {
const elementWrapper = this.find<ElementType>(selector);
return elementWrapper ? new ComponentClass(elementWrapper.getElement()) : null;
}

/**
* Returns the wrappers of all components that match the specified component type and the specified CSS selector.
* If no CSS selector is specified, returns all of the components that match the specified component type.
* If no matching component is found, returns an empty array.
*
* @param {ComponentWrapperClass} ComponentClass Component's wrapper class
* @param {string} [selector] CSS selector
* @returns `Array<Wrapper>`
*/
findAllComponents<Wrapper extends ComponentWrapper, ElementType extends HTMLElement>(
ComponentClass: ComponentWrapperClass<Wrapper, ElementType>,
selector?: string
): Array<Wrapper> {
const componentRootSelector = `.${ComponentClass.rootSelector}`;
const componentCombinedSelector = selector
? appendSelector(componentRootSelector, selector)
: componentRootSelector;

const elementWrappers = this.findAll<ElementType>(componentCombinedSelector);
return elementWrappers.map(wrapper => new ComponentClass(wrapper.getElement()));
}
}

export class ElementWrapper<ElementType extends Element = HTMLElement> extends AbstractWrapper<ElementType> {}
Expand Down
41 changes: 37 additions & 4 deletions src/core/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ const getRootSelector = (selector: string, root: string): string => {
return getUnscopedClassName(rootSelector);
};

interface WrapperClass<Wrapper> {
new (selector: string): Wrapper;
}

interface ComponentWrapperClass<Wrapper> extends WrapperClass<Wrapper> {
rootSelector: string;
}

export class AbstractWrapper implements IElementWrapper<string, MultiElementWrapper<ElementWrapper>> {
constructor(protected root: string) {}

Expand Down Expand Up @@ -35,13 +43,38 @@ export class AbstractWrapper implements IElementWrapper<string, MultiElementWrap
return this.findAll(`.${className}`);
}

findComponent<Wrapper extends ComponentWrapper>(
selector: string,
ComponentClass: { new (element: string): Wrapper }
): Wrapper {
/**
* Returns a wrapper that matches the specified component type with the specified CSS selector.
*
* Note: This function returns the specified component's wrapper even if the specified selector points to a different component type.
*
* @param {string} selector CSS selector
* @param {WrapperClass} ComponentClass Component's wrapper class
* @returns `Wrapper`
*/
findComponent<Wrapper extends ComponentWrapper>(selector: string, ComponentClass: WrapperClass<Wrapper>): Wrapper {
return new ComponentClass(this.find(selector).getElement());
}

/**
* Returns a multi-element wrapper that matches the specified component type with the specified CSS selector.
* If no CSS selector is specified, returns a multi-element wrapper that matches the specified component type.
*
* @param {string} [selector] CSS Selector
* @returns {MultiElementWrapper}
*/
findAllComponents<Wrapper extends ComponentWrapper>(
ComponentClass: ComponentWrapperClass<Wrapper>,
selector?: string
): MultiElementWrapper<Wrapper> {
const componentRootSelector = `.${ComponentClass.rootSelector}`;
const componentCombinedSelector = selector
? appendSelector(componentRootSelector, selector)
: componentRootSelector;
const rootSelector = getRootSelector(componentCombinedSelector, this.root);
return new MultiElementWrapper(rootSelector, selector => new ComponentClass(selector));
}

toSelector(): string {
return this.root;
}
Expand Down
Loading

0 comments on commit 929af94

Please sign in to comment.