Skip to content

Commit

Permalink
feat(input): add search type
Browse files Browse the repository at this point in the history
Signed-off-by: astagnol <[email protected]>
  • Loading branch information
astagnol authored and dpellier committed Nov 28, 2024
1 parent 4f6d227 commit 8b46c6b
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ function FormNative(): ReactElement {
type={ ODS_INPUT_TYPE.text }
/>

<OdsInput
isClearable={ true }
isRequired={ areAllRequired }
name="inputSearch"
type={ ODS_INPUT_TYPE.search }
/>

<OdsPassword
isClearable={ true }
isRequired={ areAllRequired }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,10 @@
&[type="number"] {
appearance: textfield;
}

&[type="search"]::-webkit-search-cancel-button,
&[type="search"]::-webkit-search-decoration {
appearance: none;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AttachInternals, Component, Element, Event, type EventEmitter, type FunctionalComponent, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core';
import { submitFormOnEnter } from '../../../../../utils/dom';
import { submitFormOnClick, submitFormOnEnter } from '../../../../../utils/dom';
import { isNumeric } from '../../../../../utils/type';
import { ODS_BUTTON_COLOR, ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT } from '../../../../button/src';
import { ODS_ICON_NAME } from '../../../../icon/src';
Expand Down Expand Up @@ -265,6 +265,7 @@ export class OdsInput {
render(): FunctionalComponent {
const hasClearableIcon = this.isClearable && !this.isLoading && !!this.value;
const hasToggleMaskIcon = this.isPassword && !this.isLoading;
const hasSearchIcon = this.type === 'search' && !this.isLoading;

return (
<Host
Expand Down Expand Up @@ -338,6 +339,20 @@ export class OdsInput {
variant={ ODS_BUTTON_VARIANT.ghost }>
</ods-button>
}
{
hasSearchIcon &&
<ods-button
icon={ ODS_ICON_NAME.magnifyingGlass }
isDisabled={ this.isDisabled || this.isReadonly }
label=""
onClick={ (event: MouseEvent): void => submitFormOnClick(event, this.internals.form) }
onKeyDown={ (event: KeyboardEvent) => this.handleKeyDown(event) }
onKeyUp={ (event: KeyboardEvent): void => submitFormOnEnter(event, this.internals.form) }
size={ ODS_BUTTON_SIZE.xs }
type={ 'submit' }
variant={ ODS_BUTTON_VARIANT.ghost }>
</ods-button>
}
</div>
</Host>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ enum ODS_INPUT_TYPE {
email = 'email',
number = 'number',
password = 'password',
search = 'search',
text = 'text',
time = 'time',
url = 'url',
Expand Down
9 changes: 9 additions & 0 deletions packages/ods/src/components/input/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@
<!-- <ods-input is-masked is-clearable>-->
<!-- </ods-input>-->


<!-- <p>Search</p>-->
<!-- <ods-input type="search">-->
<!-- </ods-input>-->

<!-- <p>Search & Clearable</p>-->
<!-- <ods-input type="search" is-clearable>-->
<!-- </ods-input>-->

<!-- <p>Pattern</p>-->
<!-- <ods-input pattern="[^12]+">-->
<!-- </ods-input>-->
Expand Down
42 changes: 42 additions & 0 deletions packages/ods/src/components/input/tests/behaviour/ods-input.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type OdsInputChangeEventDetail } from '../../src';
describe('ods-input behaviour', () => {
let el: E2EElement;
let page: E2EPage;
let buttonSearch: E2EElement;
let part: E2EElement;

async function setup(content: string): Promise<void> {
Expand All @@ -13,6 +14,7 @@ describe('ods-input behaviour', () => {
await page.evaluate(() => document.body.style.setProperty('margin', '0px'));

el = await page.find('ods-input');
buttonSearch = await page.find('ods-input >>> ods-button[icon="magnifying-glass"]');
part = await page.find('ods-input >>> [part="input"]');
await page.waitForChanges();
}
Expand Down Expand Up @@ -290,5 +292,45 @@ describe('ods-input behaviour', () => {
const url = new URL(page.url());
expect(url.searchParams.get('odsInput')).toBe('text');
});

it('should submit form on search button Enter', async() => {
await setup(`<form method="get">
<ods-input name="odsInput" type="search" value="text"></ods-input>
</form>`);

await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await page.waitForNetworkIdle();

const url = new URL(page.url());
expect(url.searchParams.get('odsInput')).toBe('text');
});

it('should submit form on search button space', async() => {
await setup(`<form method="get">
<ods-input name="odsInput" type="search" value="text"></ods-input>
</form>`);

await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Space');
await page.waitForNetworkIdle();

const url = new URL(page.url());
expect(url.searchParams.get('odsInput')).toBe('text');
});

it('should submit form on search button click', async() => {
await setup(`<form method="get">
<ods-input name="odsInput" type="search" value="text"></ods-input>
</form>`);

await buttonSearch.click();
await page.waitForNetworkIdle();

const url = new URL(page.url());
expect(url.searchParams.get('odsInput')).toBe('text');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('ods-input navigation', () => {
let input: E2EElement;
let page: E2EPage;
let buttonClearable: E2EElement;
let buttonSearch: E2EElement;
let buttonToggleMask: E2EElement;

async function isFocused(): Promise<boolean> {
Expand Down Expand Up @@ -37,6 +38,7 @@ describe('ods-input navigation', () => {
el = await page.find('ods-input');
input = await page.find('ods-input >>> input');
buttonClearable = await page.find('ods-input >>> ods-button[icon="xmark"]');
buttonSearch = await page.find('ods-input >>> ods-button[icon="magnifying-glass"]');
buttonToggleMask = await page.find('ods-input >>> ods-button[icon="eye-off"]');
}

Expand Down Expand Up @@ -289,6 +291,57 @@ describe('ods-input navigation', () => {
});
});

describe('Search', () => {
it('Button search should be focusable', async() => {
await setup('<ods-input type="search"></ods-input>');

await page.keyboard.press('Tab');
expect(await odsInputFocusedElementClassName()).toContain('ods-input__input');

await page.keyboard.press('Tab');
expect(await getFocusedButtonIconName()).toBe('magnifying-glass');

await page.keyboard.press('Tab');
expect(await odsInputFocusedElementClassName()).toBe(undefined);
});

it('should do nothing because of disabled', async() => {
await setup('<ods-input is-disabled type="search" value="value"></ods-input>');

await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Space');
await page.waitForChanges();
expect(await odsInputFocusedElementClassName()).toBe(undefined);

await page.keyboard.press('Enter');
await page.waitForChanges();
expect(await odsInputFocusedElementClassName()).toBe(undefined);

await buttonSearch.click();
await page.waitForChanges();
expect(await odsInputFocusedElementClassName()).toBe(undefined);
});

it('should do nothing because of readonly', async() => {
await setup('<ods-input is-readonly type="search" value="value"></ods-input>');

await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Space');
await page.waitForChanges();
expect(await odsInputFocusedElementClassName()).toBe(undefined);

await page.keyboard.press('Enter');
await page.waitForChanges();
expect(await odsInputFocusedElementClassName()).toBe(undefined);

await buttonSearch.click();
await page.waitForChanges();
expect(await odsInputFocusedElementClassName()).toContain('ods-input__input');
});
});

it('should have 2 button focusable with masked & clearable', async() => {
await setup('<ods-input is-masked is-clearable value="value"></ods-input>');
await page.keyboard.press('Tab');
Expand All @@ -300,4 +353,16 @@ describe('ods-input navigation', () => {
await page.keyboard.press('Tab');
expect(await getFocusedButtonIconName()).toBe('eye-off');
});

it('should have 2 button focusable with search & clearable', async() => {
await setup('<ods-input type="search" is-clearable value="value"></ods-input>');
await page.keyboard.press('Tab');
expect(await odsInputFocusedElementClassName()).toContain('ods-input__input');

await page.keyboard.press('Tab');
expect(await getFocusedButtonIconName()).toBe('xmark');

await page.keyboard.press('Tab');
expect(await getFocusedButtonIconName()).toBe('magnifying-glass');
});
});
77 changes: 77 additions & 0 deletions packages/ods/src/components/input/tests/rendering/ods-input.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { type E2EElement, type E2EPage, newE2EPage } from '@stencil/core/testing
describe('ods-input rendering', () => {
let el: E2EElement;
let page: E2EPage;
let buttonClearable: E2EElement;
let buttonSearch: E2EElement;
let buttonToggleMask: E2EElement;
let loadingSpinner: E2EElement;
let part: E2EElement;

async function setup(content: string, customStyle?: string): Promise<void> {
Expand All @@ -16,6 +20,10 @@ describe('ods-input rendering', () => {
}

el = await page.find('ods-input');
buttonClearable = await page.find('ods-input >>> ods-button[icon="xmark"]');
buttonSearch = await page.find('ods-input >>> ods-button[icon="magnifying-glass"]');
buttonToggleMask = await page.find('ods-input >>> ods-button[icon="eye-off"]');
loadingSpinner = await page.find('ods-input >>> ods-spinner');
part = await page.find('ods-input >>> [part="input"]');
await page.waitForChanges();
}
Expand Down Expand Up @@ -184,4 +192,73 @@ describe('ods-input rendering', () => {
expect(hasErrorClass2).toBe(true);
});
});

describe('isMasked', () => {
it('should render a toggle mask button', async() => {
await setup('<ods-input is-masked></ods-input>');

expect(buttonToggleMask).not.toBeNull();
});

it('should render a disabled toggle mask button when input is disabled', async() => {
await setup('<ods-input is-disabled is-masked></ods-input>');

expect(buttonToggleMask.getAttribute('is-disabled')).toBe('');
});

it('should not render a disabled toggle mask button when input is readonly', async() => {
await setup('<ods-input is-readonly is-masked></ods-input>');

expect(buttonToggleMask.getAttribute('is-disabled')).toBeNull();
});

it('should render toggle mask and clearable button', async() => {
await setup('<ods-input is-clearable is-masked value="value"></ods-input>');

expect(buttonClearable).not.toBeNull();
expect(buttonToggleMask).not.toBeNull();
});

it('should render the loading spinner when is-masked and is-loading', async() => {
await setup('<ods-input is-loading type="search"></ods-input>');

expect(buttonToggleMask).toBeNull();
expect(loadingSpinner).not.toBeNull();
});
});

describe('type search', () => {
it('should render a search button', async() => {
await setup('<ods-input type="search"></ods-input>');

expect(await el.getProperty('type')).toBe('search');
expect(buttonSearch).not.toBeNull();
});

it('should render a disabled search button when input is disabled', async() => {
await setup('<ods-input is-disabled type="search"></ods-input>');

expect(buttonSearch.getAttribute('is-disabled')).toBe('');
});

it('should render a disabled search button when input is readonly', async() => {
await setup('<ods-input is-readonly type="search"></ods-input>');

expect(buttonSearch.getAttribute('is-disabled')).toBe('');
});

it('should render search button and clearable button', async() => {
await setup('<ods-input is-clearable type="search" value="dummy"></ods-input>');

expect(buttonClearable).not.toBeNull();
expect(buttonSearch).not.toBeNull();
});

it('should render the loading spinner when input of type search is-loading', async() => {
await setup('<ods-input is-loading type="search"></ods-input>');

expect(buttonSearch).toBeNull();
expect(loadingSpinner).not.toBeNull();
});
});
});
32 changes: 32 additions & 0 deletions packages/ods/src/components/input/tests/validity/ods-input.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,22 @@ describe('ods-input validity', () => {
expect(formValidity).toBe(false);
});

it('should not submit the form on search button click before any changes if input is invalid', async() => {
await setup('<form method="get" onsubmit="return false"><ods-input is-required type="search"></ods-input></form>');

const formValidity = await page.evaluate(() => {
const form = document.querySelector<HTMLFormElement>('form');
const input = document.querySelector('ods-input');
const buttonSearch = input?.shadowRoot?.querySelector('ods-button[icon="magnifying-glass"]');

(buttonSearch as HTMLElement).click();
return form?.reportValidity();
});

expect(await el.callMethod('checkValidity')).toBe(false);
expect(formValidity).toBe(false);
});

it('should submit the form if input is valid', async() => {
await setup('<form method="get" onsubmit="return false"><ods-input is-required value="dummy"></ods-input></form>');

Expand All @@ -288,6 +304,22 @@ describe('ods-input validity', () => {
expect(await el.callMethod('checkValidity')).toBe(true);
expect(formValidity).toBe(true);
});

it('should submit the form on search button click if input is valid', async() => {
await setup('<form method="get" onsubmit="return false"><ods-input is-required type="search" value="dummy"></ods-input></form>');

const formValidity = await page.evaluate(() => {
const form = document.querySelector<HTMLFormElement>('form');
const input = document.querySelector('ods-input');
const buttonSearch = input?.shadowRoot?.querySelector('ods-button[icon="magnifying-glass"]');

(buttonSearch as HTMLElement).click();
return form?.reportValidity();
});

expect(await el.callMethod('checkValidity')).toBe(true);
expect(formValidity).toBe(true);
});
});

describe('watchers', () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/ods/src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ async function setInternalsValidityFromOdsComponent(odsFormElement: OdsFormEleme
return setInternalsValidity(odsFormElement, internals, validityState, validationMessage);
}

function submitFormOnClick(event: MouseEvent, form: HTMLFormElement | null): void {
if (event.button === 0) {
form?.requestSubmit();
}
}

function submitFormOnEnter(event: KeyboardEvent, form: HTMLFormElement | null): void {
if (event.key === 'Enter') {
form?.requestSubmit();
Expand All @@ -54,5 +60,6 @@ export {
isTargetInElement,
setInternalsValidityFromHtmlElement,
setInternalsValidityFromOdsComponent,
submitFormOnClick,
submitFormOnEnter,
};
Loading

0 comments on commit 8b46c6b

Please sign in to comment.