Skip to content

Commit

Permalink
feat(components): add text input
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasKellerer committed Apr 9, 2024
1 parent fdfa904 commit 5e1b601
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 1 deletion.
24 changes: 24 additions & 0 deletions components/src/preact/textInput/__mockData__/aggregated_hosts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"errors": [],
"info": {
"apiVersion": 1,
"dataVersion": 1709685650,
"deprecationDate": null,
"deprecationInfo": null,
"acknowledgement": null
},
"data": [
{
"host": "Homo",
"count": 123
},
{
"host": "Homo sapiens",
"count": 234
},
{
"host": "Ape",
"count": 345
}
]
}
9 changes: 9 additions & 0 deletions components/src/preact/textInput/fetchAutocompleteList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';

export async function fetchAutocompleteList(lapis: string, field: string, signal?: AbortSignal) {
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>({}, [field]);

const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;

return data.map((item) => item[field]);
}
43 changes: 43 additions & 0 deletions components/src/preact/textInput/test-input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Meta, StoryObj } from '@storybook/preact';

Check failure on line 1 in components/src/preact/textInput/test-input.stories.tsx

View workflow job for this annotation

GitHub Actions / Run linter

All imports in the declaration are only used as types. Use `import type`

Check failure on line 1 in components/src/preact/textInput/test-input.stories.tsx

View workflow job for this annotation

GitHub Actions / Run linter

There should be at least one empty line between import groups
import { TextInput, TextInputProps } from './text-input';

Check failure on line 2 in components/src/preact/textInput/test-input.stories.tsx

View workflow job for this annotation

GitHub Actions / Run linter

Imports "TextInputProps" are only used as type
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
import { LapisUrlContext } from '../LapisUrlContext';
import data from './__mockData__/aggregated_hosts.json';

Check failure on line 5 in components/src/preact/textInput/test-input.stories.tsx

View workflow job for this annotation

GitHub Actions / Run linter

`./__mockData__/aggregated_hosts.json` import should occur before import of `./text-input`

const meta: Meta<TextInputProps> = {
title: 'Input/TextInput',
component: TextInput,
parameters: {
fetchMock: {
mocks: [
{
matcher: {
name: 'hosts',
url: AGGREGATED_ENDPOINT,
query: {
fields: 'host',
},
},
response: {
status: 200,
body: data,
},
},
],
},
},
};

export default meta;

export const Default: StoryObj<TextInputProps> = {
render: (args) => (
<LapisUrlContext.Provider value={LAPIS_URL}>
<TextInput lapisField={args.lapisField} placeholderText={args.placeholderText} />
</LapisUrlContext.Provider>
),
args: {
lapisField: 'host',
placeholderText: 'Enter a host name',
},
};
72 changes: 72 additions & 0 deletions components/src/preact/textInput/text-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { FunctionComponent } from 'preact';

Check failure on line 1 in components/src/preact/textInput/text-input.tsx

View workflow job for this annotation

GitHub Actions / Run linter

All imports in the declaration are only used as types. Use `import type`
import { useContext, useRef } from 'preact/hooks';

Check failure on line 2 in components/src/preact/textInput/text-input.tsx

View workflow job for this annotation

GitHub Actions / Run linter

There should be at least one empty line between import groups
import { LapisUrlContext } from '../LapisUrlContext';

Check failure on line 3 in components/src/preact/textInput/text-input.tsx

View workflow job for this annotation

GitHub Actions / Run linter

`../LapisUrlContext` import should occur after import of `./fetchAutocompleteList`
import { useQuery } from '../useQuery';

Check failure on line 4 in components/src/preact/textInput/text-input.tsx

View workflow job for this annotation

GitHub Actions / Run linter

`../useQuery` import should occur after import of `../components/no-data-display`
import { fetchAutocompleteList } from './fetchAutocompleteList';
import { LoadingDisplay } from '../components/loading-display';

Check failure on line 6 in components/src/preact/textInput/text-input.tsx

View workflow job for this annotation

GitHub Actions / Run linter

`../components/loading-display` import should occur after import of `../components/error-display`
import { ErrorDisplay } from '../components/error-display';
import { NoDataDisplay } from '../components/no-data-display';

export interface TextInputProps {
lapisField: string;
placeholderText?: string;
}

export const TextInput: FunctionComponent<TextInputProps> = ({ lapisField, placeholderText }) => {
const lapis = useContext(LapisUrlContext);

const inputRef = useRef<HTMLInputElement>(null);

const { data, error, isLoading } = useQuery(() => fetchAutocompleteList(lapis, lapisField), [lapisField, lapis]);

if (isLoading) {
return <LoadingDisplay />;
}

if (error !== null) {
return <ErrorDisplay error={error} />;
}

if (data === null) {
return <NoDataDisplay />;
}

const onInput = () => {
const value = inputRef.current?.value;

if (isValidValue(value)) {
inputRef.current?.dispatchEvent(
new CustomEvent('gs-text-input-changed', {
detail: { [lapisField]: value },
bubbles: true,
composed: true,
}),
);
}
};

const isValidValue = (value: string | undefined) => {
if (value === undefined) {
return false;
}
return data.includes(value);
};

return (
<>
<input
type='text'
class='input input-bordered'
placeholder={placeholderText !== undefined ? placeholderText : lapisField}
onInput={onInput}
ref={inputRef}
list={lapisField}
/>
<datalist id={lapisField}>
{data.map((item) => (
<option value={item} key={item} />
))}
</datalist>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { html } from 'lit';

import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
import '../app';
import '../input/location-filter-component';
import './location-filter-component';
import data from '../../preact/locationFilter/__mockData__/aggregated.json';
import { withinShadowRoot } from '../withinShadowRoot.story';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { Meta, StoryObj } from '@storybook/web-components';

Check failure on line 1 in components/src/web-components/input/text-input-component.stories.ts

View workflow job for this annotation

GitHub Actions / Run linter

There should be at least one empty line between import groups
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';

import { html } from 'lit';
import '../app';
import './text-input-component';
import { withinShadowRoot } from '../withinShadowRoot.story';
import { expect, fn, userEvent, waitFor } from '@storybook/test';
import data from '../../preact/textInput/__mockData__/aggregated_hosts.json';

const meta: Meta = {
title: 'Input/Text input',
component: 'gs-text-input',
parameters: {
actions: {
handles: ['gs-text-input-changed'],
},
fetchMock: {
mocks: [
{
matcher: {
name: 'hosts',
url: AGGREGATED_ENDPOINT,
query: {
fields: 'host',
},
},
response: {
status: 200,
body: data,
},
},
],
},
},
};

export default meta;

export const Default: StoryObj<{ lapisField: string; placeholderText: string }> = {
render: (args) => {
return html` <gs-app lapis="${LAPIS_URL}">
<div class="max-w-screen-lg">
<gs-text-input .lapisField=${args.lapisField} .placeholderText=${args.placeholderText}></gs-text-input>
</div>
</gs-app>`;
},
args: {
lapisField: 'host',
placeholderText: 'Enter host name',
},
};

export const FiresEvent: StoryObj<{ lapisField: string; placeholderText: string }> = {
...Default,
play: async ({ canvasElement, step }) => {
const canvas = await withinShadowRoot(canvasElement, 'gs-text-input');

const inputField = () => canvas.getByPlaceholderText('Enter host name');
const listenerMock = fn();
await step('Setup event listener mock', async () => {
canvasElement.addEventListener('gs-text-input-changed', listenerMock);
});

await step('wait until data is loaded', async () => {
await waitFor(() => {
return expect(inputField()).toBeEnabled();
});
});

await step('Enters an invalid host name', async () => {
await userEvent.type(inputField(), 'notInList');
await expect(listenerMock).not.toHaveBeenCalled();
await userEvent.type(inputField(), '{backspace>9/}');
});

await step('Enter a valid host name', async () => {
await userEvent.type(inputField(), 'Homo');

await expect(listenerMock).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
host: 'Homo',
},
}),
);
});
},
};
22 changes: 22 additions & 0 deletions components/src/web-components/input/text-input-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { PreactLitAdapter } from '../PreactLitAdapter';
import { customElement, property } from 'lit/decorators.js';
import { TextInput } from '../../preact/textInput/text-input';

@customElement('gs-text-input')
export class TextInputComponent extends PreactLitAdapter {
@property()
lapisField = '';

@property()
placeholderText = '';

override render() {
return <TextInput lapisField={this.lapisField} placeholderText={this.placeholderText} />;
}
}

declare global {
interface HTMLElementTagNameMap {
'gs-text-input': TextInputComponent;
}
}

0 comments on commit 5e1b601

Please sign in to comment.