Skip to content

Commit

Permalink
feat(search): add new component
Browse files Browse the repository at this point in the history
  • Loading branch information
nowseemee committed May 30, 2023
1 parent b00667a commit 0dee00a
Show file tree
Hide file tree
Showing 14 changed files with 1,064 additions and 85 deletions.
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@telekom/design-tokens": "^1.0.0-beta.4",
"@telekom/scale-design-tokens": "^3.0.0-beta.134",
"classnames": "^2.2.6",
"highlight-words-core": "^1.2.2",
"stencil-inline-svg": "^1.0.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
--input-font-family: inherit;
--input-font-size: var(--telekom-typography-font-size-body);
--input-background-color: var(--telekom-color-ui-state-fill-standard);
--input-color: var(--telekom-color-text-and-icon-standard);

/* interactive-icon */
--interactive-icon-color: var(--telekom-color-text-and-icon-additional);
Expand Down Expand Up @@ -104,6 +105,7 @@
font-family: var(--input-font-family);
font-size: var(--input-font-size);
background-color: var(--input-background-color);
color: var(--input-color);
}

[part~='interactive-icon'] {
Expand All @@ -112,6 +114,10 @@
color: var(--interactive-icon-color);
}

[part~='label'] {
visibility: hidden;
}

[part~='clear-icon-button'] {
margin-left: var(--clear-icon-button-margin-left);
margin-right: var(--clear-icon-button-margin-right);
Expand Down
119 changes: 53 additions & 66 deletions packages/components/src/components/search-input/search-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ import {
import classNames from 'classnames';
import { emitEvent, generateUniqueId } from '../../utils/utils';

interface InputChangeEventDetail {
value: string | undefined | null;
}

@Component({
tag: 'scale-search-input',
styleUrl: './search-input.css',
shadow: true,
})
export class SearchInput {
@Element() hostElement: HTMLElement;

/** (optional) Input name */
@Prop() name?: string = 'Search';
@Prop() name?: string = 'search';

/** (optional) Input label */
@Prop() label?: string = 'Search';
/** (optional) Input status */
@Prop() invalid?: boolean = false;
/** (optional) Input text string max length */
Expand All @@ -48,34 +48,23 @@ export class SearchInput {
/** (optional) Input required */
@Prop() required?: boolean;
/** (optional) Input value */
@Prop({ mutable: true }) value?: string | null = '';
/** (optional) Input id */
@Prop() inputId?: string;
@Prop({ mutable: true, reflect: true }) value?: string | null = '';
/** (optional) input background transparent */
@Prop() transparent?: boolean;
/** (optional) the input should automatically get focus when the page loads. */
@Prop() inputAutofocus?: boolean;
/** (optional) custom value for autocomplete HTML attribute */
@Prop() inputAutocomplete?: string;
/** (optional) id or space separated list of ids of elements that provide or link to additional related information. */
@Prop() ariaDetailedId?: string;
/** (optional) to avoid displaying the label */
@Prop() hideLabelVisually?: boolean = false;
/** (optional) Injected CSS styles */
@Prop() styles?: string;

/** (optional)) Makes type `input` behave as a controlled component in React */
@Prop() experimentalControlled?: boolean = false;
/** Emitted when a keyboard input occurred. */
@Event({ eventName: 'scale-input' }) scaleInput!: EventEmitter<KeyboardEvent>;
/** Emitted when the value has changed. */
@Event({ eventName: 'scale-change' })
scaleChange!: EventEmitter<InputChangeEventDetail>;
@Prop() innerAriaExpanded: string;
@Prop({ mutable: true, reflect: true }) inputId: string;
/** Emitted when the input has focus. */
@Event({ eventName: 'scale-focus' }) scaleFocus!: EventEmitter<void>;
/** Emitted when the input loses focus. */
@Event({ eventName: 'scale-blur' }) scaleBlur!: EventEmitter<void>;
/** Emitted when the input has focus. */
@Event({ eventName: 'scale-focus-out' }) scaleFocusout!: EventEmitter<void>;
/** Emitted on keydown. */
@Event({ eventName: 'scale-keydown' })
scaleKeyDown!: EventEmitter<KeyboardEvent>;
Expand All @@ -89,7 +78,14 @@ export class SearchInput {
/** "forceUpdate" hack, set it to trigger and re-render */
@State() forceUpdate: string;

/** (optional) Input helper text */
@Prop() helperText?: string = '';
/** (optional) Variant */
@Prop() variant?: 'informational' | 'warning' | 'danger' | 'success' =
'informational';

private readonly internalId = generateUniqueId();
private inputElement: HTMLInputElement;

componentWillLoad() {
if (this.inputId == null) {
Expand All @@ -98,57 +94,29 @@ export class SearchInput {
}

componentDidRender() {
// When `experimentalControlled` is true,
// make sure the <input> is always in sync with the value.
const value = this.value == null ? '' : this.value.toString();
const input = this.hostElement.querySelector('input');
if (this.experimentalControlled && input.value.toString() !== value) {
input.value = value;
}
// // When `experimentalControlled` is true,
// // make sure the <input> is always in sync with the value.
// const value = this.value == null ? '' : this.value.toString();
// const input = this.hostElement.querySelector('input');
// if (this.experimentalControlled && input.value.toString() !== value) {
// input.value = value;
// }
}

handleInput = (event: Event) => {
const target = event.target as HTMLInputElement | null;
if (target) {
this.value = target.value || '';
this.emitChange();
}
if (this.experimentalControlled) {
this.hostElement.querySelector('input').value = String(this.value);
this.forceUpdate = String(Date.now());
}
emitEvent(this, 'scaleInput', event as KeyboardEvent);
};

handleChange = (event: Event) => {
const target = event.target as HTMLInputElement | null;
if (target) {
this.value = target.value || '';
this.emitChange();
}
};

handleFocus = () => {
this.hasFocus = true;
emitEvent(this, 'scaleFocus');
};

handleFocusout = () => {
this.hasFocus = false;
emitEvent(this, 'scaleFocusout');
};

emitChange() {
emitEvent(this, 'scaleChange', {
value: this.value == null ? this.value : this.value.toString(),
});
}

emitBlur = () => {
this.hasFocus = false;
emitEvent(this, 'scaleBlur');
};

emitKeyDown = (event: KeyboardEvent) => {
setTimeout(() => {
this.value = this.inputElement?.value;
});
emitEvent(this, 'scaleKeyDown', event);
};

Expand All @@ -161,7 +129,7 @@ export class SearchInput {
<scale-icon-button
size="medium"
part="clear-icon-button"
onClick={() => (this.value = '')}
onClick={() => (this.inputElement.value = '')}
>
<scale-icon-action-close
part="clear-icon"
Expand All @@ -173,21 +141,34 @@ export class SearchInput {
}

render() {
const helperTextId = `helper-message-${this.internalId}`;
const ariaDescribedByAttr = { 'aria-describedBy': helperTextId };
const ariaDetailedById = { 'aria-details': this.ariaDetailedId };
const ariaInvalidAttr = this.invalid ? { 'aria-invalid': true } : {};

const basePart = classNames(
'base',
this.hasFocus && 'focus',
this.disabled && 'disabled'
);
return (
<Host>
{this.styles && <style>{this.styles}</style>}
<div part={basePart}>
<slot name="prefix-icon" />
<label id={`${this.inputId}-label`} part="label">
{this.label}
</label>
<input
ref={(ref) => (this.inputElement = ref)}
aria-owns={`${this.inputId}-listbox`}
aria-expanded={this.innerAriaExpanded}
aria-labelledby={`${this.inputId}-label`}
aria-haspopup="listbox"
aria-autocomplete="list"
type="search"
inputMode="search"
part="input"
role="combobox"
placeholder={this.placeholder}
value={this.value}
{...(!!this.name ? { name: this.name } : {})}
Expand All @@ -196,17 +177,16 @@ export class SearchInput {
minLength={this.minLength}
maxLength={this.maxLength}
id={this.inputId}
onInput={this.handleInput}
onChange={this.handleChange}
onFocus={this.handleFocus}
onFocusout={this.handleFocusout}
onKeyDown={this.emitKeyDown}
onBlur={this.emitBlur}
disabled={this.disabled}
autocomplete={this.inputAutocomplete}
autocomplete="off"
{...ariaDetailedById}
{...ariaInvalidAttr}
{...(this.helperText ? ariaDescribedByAttr : {})}
></input>
{this.value ? (
{this.inputElement?.value ? (
this.getClearIconButton()
) : (
<div
Expand All @@ -217,6 +197,13 @@ export class SearchInput {
</div>
)}
</div>
{this.helperText && (
<scale-helper-text
id={helperTextId}
helperText={this.helperText}
variant={this.invalid ? 'danger' : this.variant}
></scale-helper-text>
)}
</Host>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
:host {
/*listbox*/
--background-listbox: var(--telekom-color-background-surface);
--box-shadow-listbox: 0 2px 4px 0 rgba(0, 0, 0, 0.1),
0 4px 16px 0 rgba(0, 0, 0, 0.1);
--max-height-listbox: 300px;
--z-index-listbox: 99;
--radius: var(--telekom-radius-standard);
}
/*listbox*/
[part='listbox'] {
position: relative;
}

[part='listbox-scroll-container'] {
max-height: var(--max-height-listbox);
overflow-y: auto;
}

[part='listbox-pad'] {
background: var(--background-listbox);
box-shadow: var(--box-shadow-listbox);
border-radius: var(--radius);
padding: var(--radius) 0;
margin-top: var(--telekom-line-weight-highlight);
left: 0;
position: absolute;
top: 100%;
width: 100%;
z-index: var(--z-index-listbox);
display: none;
}

:host([open]) [part='listbox-pad'] {
display: block;
}

[part~='transparent'] [part='listbox'] {
background-color: transparent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Component, h, Host, Prop } from '@stencil/core';

@Component({
tag: 'scale-search-list-box',
styleUrl: 'search-list-box.css',
shadow: true,
})
export class SearchListBox {
@Prop() open?: boolean;
@Prop() comboboxId?: string = 'combobox';
@Prop() refListBoxPadEl: any;
@Prop() refListBoxEl: any;

render() {
return (
<Host open={this.open}>
<div part="listbox-pad" ref={this.refListBoxPadEl}>
<div part="listbox-scroll-container">
<div
part="listbox"
role="listbox"
id={`${this.comboboxId}-listbox`}
aria-labelledby={`${this.comboboxId}-label`}
tabindex="-1"
>
<slot></slot>
</div>
</div>
</div>
</Host>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
:host {
}

:host [part='head'] {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 8px 8px 16px;
color: var(--telekom-color-text-and-icon-additional);
font: var(--telekom-text-style-small-bold);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Component, Element, h, Host, Prop } from '@stencil/core';

@Component({
tag: 'scale-search-list-category',
styleUrl: 'search-list-category.css',
shadow: true,
})
export class SearchListCategory {
@Element() hostElement: HTMLElement;

@Prop() refListBoxEl: any;

render() {
return (
<Host>
<div part="base">
<div part="head">
<slot name="title"></slot>
<slot name="action"></slot>
</div>
<slot></slot>
</div>
</Host>
);
}
}
Loading

0 comments on commit 0dee00a

Please sign in to comment.