Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add OK search-input",
"packageName": "@ni/ok-angular",
"email": "1458528+fredvisser@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add OK search-input",
"packageName": "@ni/ok-components",
"email": "1458528+fredvisser@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { NimbleRichTextViewerModule } from '@ni/nimble-angular/rich-text/viewer'
import { NimbleRichTextEditorModule } from '@ni/nimble-angular/rich-text/editor';
import { NimbleRichTextMentionUsersModule } from '@ni/nimble-angular/rich-text-mention/users';
import { OkButtonModule } from 'ok-angular/button/ok-button.module';
import { OkSearchInputModule } from 'ok-angular/search-input/ok-search-input.module';
import { SprightChatConversationModule } from '@ni/spright-angular/chat/conversation';
import { SprightChatInputModule } from '@ni/spright-angular/chat/input';
import { SprightIconWorkItemCalendarWeekDirective } from '@ni/spright-angular/icons/work-item-calendar-week';
Expand Down Expand Up @@ -121,6 +122,7 @@ import { CustomAppComponent } from './customapp/customapp.component';
NimbleIconPencilToRectangleModule,
NimbleIconMessagesSparkleModule,
OkButtonModule,
OkSearchInputModule,
SprightChatConversationModule,
SprightChatInputModule,
SprightChatMessageInboundModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,5 +519,9 @@
<div class="container-label">Button (Ok)</div>
<ok-button>Ok</ok-button>
</div>
<div class="sub-container">
<div class="container-label">Search Input (Ok)</div>
<ok-search-input placeholder="Search assets" value="PXI"></ok-search-input>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "public-api.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
import type { SearchInput } from '@ni/ok-components/dist/esm/search-input';
import { searchInputTag } from '@ni/ok-components/dist/esm/search-input';
import type { SearchInputAppearance } from '@ni/ok-components/dist/esm/search-input/types';

export type { SearchInput };
export { searchInputTag };

/**
* Directive to provide Angular integration for the search input.
*/
@Directive({
selector: 'ok-search-input',
standalone: false
})
export class OkSearchInputDirective {
@Input()
public set appearance(value: SearchInputAppearance) {
this.renderer.setProperty(this.elementRef.nativeElement, 'appearance', value);
}

@Input()
public set placeholder(value: string) {
this.renderer.setProperty(this.elementRef.nativeElement, 'placeholder', value);
}

@Input()
public set value(value: string) {
this.renderer.setProperty(this.elementRef.nativeElement, 'value', value);
}

public constructor(
private readonly elementRef: ElementRef<SearchInput>,
private readonly renderer: Renderer2
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { OkSearchInputDirective } from './ok-search-input.directive';

import '@ni/ok-components/dist/esm/search-input';

@NgModule({
declarations: [OkSearchInputDirective],
imports: [CommonModule],
exports: [OkSearchInputDirective]
})
export class OkSearchInputModule { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ok-search-input.directive';
export * from './ok-search-input.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TestBed } from '@angular/core/testing';
import { OkSearchInputModule } from '../ok-search-input.module';

describe('Ok search input', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [OkSearchInputModule]
});
});

it('custom element is defined', () => {
expect(customElements.get('ok-search-input')).not.toBeUndefined();
});
});
1 change: 1 addition & 0 deletions packages/ok-components/src/all-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ import '@ni/spright-components/dist/esm/all-components';

import './button';
import './icon-dynamic';
import './search-input';
64 changes: 64 additions & 0 deletions packages/ok-components/src/search-input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { attr } from '@ni/fast-element';
import { DesignSystem, FoundationElement } from '@ni/fast-foundation';
import { styles } from './styles';
import { template } from './template';
import {
SearchInputAppearance,
type SearchInputAppearance as SearchInputAppearanceType
} from './types';

declare global {
interface HTMLElementTagNameMap {
'ok-search-input': SearchInput;
}
}

/**
* A compact search input with a built-in clear affordance.
*/
export class SearchInput extends FoundationElement {
@attr
public placeholder = 'Search';

@attr
public value = '';

@attr
public appearance: SearchInputAppearanceType = SearchInputAppearance.outline;

private inputElement: HTMLInputElement | null = null;

public handleInput(event: Event): void {
const input = event.target as HTMLInputElement;
this.value = input.value;
}

public handleChange(event: Event): void {
const input = event.target as HTMLInputElement;
this.value = input.value;
}

public captureInputRef(element: HTMLInputElement | null): void {
this.inputElement = element;
}

public clear(): void {
if (this.value === '') {
return;
}

this.value = '';
this.inputElement?.focus();
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
}
}

const okSearchInput = SearchInput.compose({
baseName: 'search-input',
template,
styles
});

DesignSystem.getOrCreate().withPrefix('ok').register(okSearchInput());
export const searchInputTag = 'ok-search-input';
161 changes: 161 additions & 0 deletions packages/ok-components/src/search-input/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { css } from '@ni/fast-element';
import {
bodyFont,
bodyFontColor,
borderHoverColor,
borderRgbPartialColor,
borderWidth,
controlHeight,
fillHoverColor,
iconColor,
placeholderFontColor,
smallDelay,
standardPadding
} from '@ni/nimble-components/dist/esm/theme-provider/design-tokens';
import { display } from '../utilities/style/display';

export const styles = css`
${display('inline-block')}

:host {
--ok-search-input-height: ${controlHeight};
--ok-search-input-inline-padding: ${standardPadding};
--ok-search-input-leading-space: calc(var(--ok-search-input-inline-padding) + 16px);
--ok-search-input-trailing-space: calc(var(--ok-search-input-inline-padding) + 20px);
--ok-search-input-border-color: rgba(${borderRgbPartialColor}, 0.3);
--ok-search-input-border-radius: 0px;
min-width: 120px;
font: ${bodyFont};
color: ${bodyFontColor};
}

.search-input-container {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: var(--ok-search-input-height);
box-sizing: border-box;
border: ${borderWidth} solid transparent;
border-radius: var(--ok-search-input-border-radius);
color: inherit;
background-color: transparent;
transition:
border-color ${smallDelay} ease-in-out,
box-shadow ${smallDelay} ease-in-out,
background-color ${smallDelay} ease-in-out;
}

.search-input-container::after {
content: '';
position: absolute;
inset-inline: 0;
inset-block-end: calc(-1 * ${borderWidth});
border-bottom: calc(${borderWidth} + 1px) solid ${borderHoverColor};
transform: scaleX(0);
transform-origin: center;
transition: transform ${smallDelay} ease-in-out;
pointer-events: none;
}

.search-input {
-webkit-appearance: none;
appearance: none;
display: block;
flex: 1 1 auto;
min-width: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 0 var(--ok-search-input-trailing-space) 0 var(--ok-search-input-leading-space);
font: inherit;
line-height: normal;
color: inherit;
border: none;
outline: none;
border-radius: 0;
background: transparent;
}

.search-input::placeholder {
color: ${placeholderFontColor};
}

.search-input-icon,
.search-input-clear {
position: absolute;
display: inline-flex;
align-items: center;
justify-content: center;
top: 50%;
transform: translateY(-50%);
color: ${placeholderFontColor};
${iconColor.cssCustomProperty}: ${placeholderFontColor};
}

.search-input-icon {
inset-inline-start: var(--ok-search-input-inline-padding);
pointer-events: none;
}

.search-input-clear {
-webkit-appearance: none;
appearance: none;
inset-inline-end: 2px;
width: calc(var(--ok-search-input-height) - 6px);
height: calc(var(--ok-search-input-height) - 6px);
padding: 0;
border: none;
border-radius: 2px;
background: transparent;
cursor: pointer;
}

.search-input-clear:hover {
background: ${fillHoverColor};
}

.search-input-clear:focus-visible {
outline: ${borderWidth} solid ${borderHoverColor};
outline-offset: -1px;
}

.search-input:focus-visible {
outline: none;
}

:host([appearance='outline']) .search-input-container {
border-color: var(--ok-search-input-border-color);
}

:host([appearance='outline']) .search-input-container:hover,
:host([appearance='outline']) .search-input-container:focus-within {
border-color: ${borderHoverColor};
box-shadow: 0 0 0 ${borderWidth} ${borderHoverColor} inset;
}

:host([appearance='block']) .search-input-container {
background-color: rgba(${borderRgbPartialColor}, 0.1);
}

:host([appearance='block']) .search-input-container:hover::after,
:host([appearance='block']) .search-input-container:focus-within::after,
:host([appearance='ghost']) .search-input-container::after,
:host([appearance='super-ghost']) .search-input-container:hover::after,
:host([appearance='super-ghost']) .search-input-container:focus-within::after {
transform: scaleX(1);
}

:host([appearance='ghost']) .search-input-container::after {
border-bottom-color: var(--ok-search-input-border-color);
}

:host([appearance='ghost']) .search-input-container:hover::after,
:host([appearance='ghost']) .search-input-container:focus-within::after,
:host([appearance='super-ghost']) .search-input-container:hover::after,
:host([appearance='super-ghost']) .search-input-container:focus-within::after,
:host([appearance='block']) .search-input-container:hover::after,
:host([appearance='block']) .search-input-container:focus-within::after {
border-bottom-color: ${borderHoverColor};
}
`;
34 changes: 34 additions & 0 deletions packages/ok-components/src/search-input/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { html, ref, when } from '@ni/fast-element';
import { iconMagnifyingGlassTag } from '@ni/nimble-components/dist/esm/icons/magnifying-glass';
import { iconTimesTag } from '@ni/nimble-components/dist/esm/icons/times';
import type { SearchInput } from '.';

export const template = html<SearchInput>`
<div class="search-input-container">
<span class="search-input-icon" aria-hidden="true">
<${iconMagnifyingGlassTag}></${iconMagnifyingGlassTag}>
</span>
<input
class="search-input"
type="text"
placeholder="${x => x.placeholder}"
:value="${x => x.value}"
${ref('captureInputRef')}
@input="${(x, c) => x.handleInput(c.event)}"
@change="${(x, c) => x.handleChange(c.event)}"
/>
${when(
x => x.value.length > 0,
html<SearchInput>`
<button
class="search-input-clear"
type="button"
aria-label="Clear search"
@click="${x => x.clear()}"
>
<${iconTimesTag}></${iconTimesTag}>
</button>
`
)}
</div>
`;
Loading
Loading