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 <a.stagnol@gmail.com>
  • 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.
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy