From c02b09c51abc6113185e8359c4b54239a9a3e77e Mon Sep 17 00:00:00 2001 From: Fred Visser <1458528+fredvisser@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:24:48 -0500 Subject: [PATCH 1/2] Add OK search-input --- ...-09482ac4-a62c-47fc-be43-c52b823e870e.json | 7 + ...-6037eb00-1211-4817-b88c-f456f2305a49.json | 7 + .../example-client-app/src/app/app.module.ts | 2 + .../app/customapp/customapp.component.html | 4 + .../ok-angular/search-input/ng-package.json | 6 + .../search-input/ok-search-input.directive.ts | 36 ++++ .../search-input/ok-search-input.module.ts | 12 ++ .../ok-angular/search-input/public-api.ts | 2 + .../tests/search-input.directive.spec.ts | 14 ++ packages/ok-components/src/all-components.ts | 1 + .../ok-components/src/search-input/index.ts | 64 +++++++ .../ok-components/src/search-input/styles.ts | 161 ++++++++++++++++++ .../src/search-input/template.ts | 34 ++++ .../search-input/tests/search-input.spec.ts | 95 +++++++++++ .../ok-components/src/search-input/types.ts | 8 + .../search-input-matrix.stories.ts | 97 +++++++++++ .../src/ok/search-input/search-input.mdx | 15 ++ .../ok/search-input/search-input.stories.ts | 71 ++++++++ 18 files changed, 636 insertions(+) create mode 100644 change/@ni-ok-angular-09482ac4-a62c-47fc-be43-c52b823e870e.json create mode 100644 change/@ni-ok-components-6037eb00-1211-4817-b88c-f456f2305a49.json create mode 100644 packages/angular-workspace/ok-angular/search-input/ng-package.json create mode 100644 packages/angular-workspace/ok-angular/search-input/ok-search-input.directive.ts create mode 100644 packages/angular-workspace/ok-angular/search-input/ok-search-input.module.ts create mode 100644 packages/angular-workspace/ok-angular/search-input/public-api.ts create mode 100644 packages/angular-workspace/ok-angular/search-input/tests/search-input.directive.spec.ts create mode 100644 packages/ok-components/src/search-input/index.ts create mode 100644 packages/ok-components/src/search-input/styles.ts create mode 100644 packages/ok-components/src/search-input/template.ts create mode 100644 packages/ok-components/src/search-input/tests/search-input.spec.ts create mode 100644 packages/ok-components/src/search-input/types.ts create mode 100644 packages/storybook/src/ok/search-input/search-input-matrix.stories.ts create mode 100644 packages/storybook/src/ok/search-input/search-input.mdx create mode 100644 packages/storybook/src/ok/search-input/search-input.stories.ts diff --git a/change/@ni-ok-angular-09482ac4-a62c-47fc-be43-c52b823e870e.json b/change/@ni-ok-angular-09482ac4-a62c-47fc-be43-c52b823e870e.json new file mode 100644 index 0000000000..878a54a550 --- /dev/null +++ b/change/@ni-ok-angular-09482ac4-a62c-47fc-be43-c52b823e870e.json @@ -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" +} diff --git a/change/@ni-ok-components-6037eb00-1211-4817-b88c-f456f2305a49.json b/change/@ni-ok-components-6037eb00-1211-4817-b88c-f456f2305a49.json new file mode 100644 index 0000000000..719a26842e --- /dev/null +++ b/change/@ni-ok-components-6037eb00-1211-4817-b88c-f456f2305a49.json @@ -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" +} diff --git a/packages/angular-workspace/example-client-app/src/app/app.module.ts b/packages/angular-workspace/example-client-app/src/app/app.module.ts index 397d5a7efe..9a08cbd981 100644 --- a/packages/angular-workspace/example-client-app/src/app/app.module.ts +++ b/packages/angular-workspace/example-client-app/src/app/app.module.ts @@ -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'; @@ -121,6 +122,7 @@ import { CustomAppComponent } from './customapp/customapp.component'; NimbleIconPencilToRectangleModule, NimbleIconMessagesSparkleModule, OkButtonModule, + OkSearchInputModule, SprightChatConversationModule, SprightChatInputModule, SprightChatMessageInboundModule, diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/customapp.component.html b/packages/angular-workspace/example-client-app/src/app/customapp/customapp.component.html index 4458204fde..b906de9546 100644 --- a/packages/angular-workspace/example-client-app/src/app/customapp/customapp.component.html +++ b/packages/angular-workspace/example-client-app/src/app/customapp/customapp.component.html @@ -519,5 +519,9 @@
Button (Ok)
Ok +
+
Search Input (Ok)
+ +
diff --git a/packages/angular-workspace/ok-angular/search-input/ng-package.json b/packages/angular-workspace/ok-angular/search-input/ng-package.json new file mode 100644 index 0000000000..0cd0286f2e --- /dev/null +++ b/packages/angular-workspace/ok-angular/search-input/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} \ No newline at end of file diff --git a/packages/angular-workspace/ok-angular/search-input/ok-search-input.directive.ts b/packages/angular-workspace/ok-angular/search-input/ok-search-input.directive.ts new file mode 100644 index 0000000000..3fba48e454 --- /dev/null +++ b/packages/angular-workspace/ok-angular/search-input/ok-search-input.directive.ts @@ -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, + private readonly renderer: Renderer2 + ) {} +} \ No newline at end of file diff --git a/packages/angular-workspace/ok-angular/search-input/ok-search-input.module.ts b/packages/angular-workspace/ok-angular/search-input/ok-search-input.module.ts new file mode 100644 index 0000000000..2eb0b5b0a5 --- /dev/null +++ b/packages/angular-workspace/ok-angular/search-input/ok-search-input.module.ts @@ -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 { } \ No newline at end of file diff --git a/packages/angular-workspace/ok-angular/search-input/public-api.ts b/packages/angular-workspace/ok-angular/search-input/public-api.ts new file mode 100644 index 0000000000..98cadeb10c --- /dev/null +++ b/packages/angular-workspace/ok-angular/search-input/public-api.ts @@ -0,0 +1,2 @@ +export * from './ok-search-input.directive'; +export * from './ok-search-input.module'; \ No newline at end of file diff --git a/packages/angular-workspace/ok-angular/search-input/tests/search-input.directive.spec.ts b/packages/angular-workspace/ok-angular/search-input/tests/search-input.directive.spec.ts new file mode 100644 index 0000000000..9eb87457db --- /dev/null +++ b/packages/angular-workspace/ok-angular/search-input/tests/search-input.directive.spec.ts @@ -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(); + }); +}); \ No newline at end of file diff --git a/packages/ok-components/src/all-components.ts b/packages/ok-components/src/all-components.ts index a07b59ba2d..7853556071 100644 --- a/packages/ok-components/src/all-components.ts +++ b/packages/ok-components/src/all-components.ts @@ -8,3 +8,4 @@ import '@ni/spright-components/dist/esm/all-components'; import './button'; import './icon-dynamic'; +import './search-input'; diff --git a/packages/ok-components/src/search-input/index.ts b/packages/ok-components/src/search-input/index.ts new file mode 100644 index 0000000000..38f9c0a4a5 --- /dev/null +++ b/packages/ok-components/src/search-input/index.ts @@ -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'; \ No newline at end of file diff --git a/packages/ok-components/src/search-input/styles.ts b/packages/ok-components/src/search-input/styles.ts new file mode 100644 index 0000000000..5646d542c9 --- /dev/null +++ b/packages/ok-components/src/search-input/styles.ts @@ -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}; + } +`; \ No newline at end of file diff --git a/packages/ok-components/src/search-input/template.ts b/packages/ok-components/src/search-input/template.ts new file mode 100644 index 0000000000..7d0200d408 --- /dev/null +++ b/packages/ok-components/src/search-input/template.ts @@ -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` +
+ + + ${when( + x => x.value.length > 0, + html` + + ` + )} +
+`; \ No newline at end of file diff --git a/packages/ok-components/src/search-input/tests/search-input.spec.ts b/packages/ok-components/src/search-input/tests/search-input.spec.ts new file mode 100644 index 0000000000..d2cd2532ee --- /dev/null +++ b/packages/ok-components/src/search-input/tests/search-input.spec.ts @@ -0,0 +1,95 @@ +import { html } from '@ni/fast-element'; +import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers'; +import { fixture, type Fixture } from '../../utilities/tests/fixture'; +import { SearchInput, searchInputTag } from '..'; +import { SearchInputAppearance } from '../types'; + +async function setup(): Promise> { + return await fixture(html` + <${searchInputTag} placeholder="Search assets"> + `); +} + +describe('SearchInput', () => { + let element: SearchInput; + let connect: () => Promise; + let disconnect: (() => Promise) | undefined; + + afterEach(async () => { + await disconnect?.(); + disconnect = undefined; + }); + + it('can construct an element instance', () => { + expect(document.createElement(searchInputTag)).toBeInstanceOf(SearchInput); + }); + + it('renders the configured placeholder', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + await waitForUpdatesAsync(); + + const input = element.shadowRoot?.querySelector('input'); + expect(input?.getAttribute('placeholder')).toBe('Search assets'); + }); + + it('uses a plain text input with a single custom clear button affordance', async () => { + ({ element, connect, disconnect } = await setup()); + element.value = 'asset'; + await connect(); + await waitForUpdatesAsync(); + + const input = element.shadowRoot?.querySelector('input'); + expect(input?.getAttribute('type')).toBe('text'); + expect(element.shadowRoot?.querySelectorAll('.search-input-clear').length).toBe(1); + }); + + it('clears the value when the clear button is clicked', async () => { + ({ element, connect, disconnect } = await setup()); + element.value = 'asset'; + await connect(); + await waitForUpdatesAsync(); + + const clearButton = element.shadowRoot?.querySelector('.search-input-clear'); + clearButton?.click(); + await waitForUpdatesAsync(); + + expect(element.value).toBe(''); + }); + + it('emits one input and one change event for a single input interaction', async () => { + ({ element, connect, disconnect } = await setup()); + const inputSpy = jasmine.createSpy('input'); + const changeSpy = jasmine.createSpy('change'); + await connect(); + await waitForUpdatesAsync(); + + element.addEventListener('input', inputSpy); + element.addEventListener('change', changeSpy); + + const input = element.shadowRoot?.querySelector('input'); + input!.value = 'asset'; + input!.dispatchEvent(new Event('input', { bubbles: true, composed: true })); + input!.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + await waitForUpdatesAsync(); + + expect(inputSpy).toHaveBeenCalledTimes(1); + expect(changeSpy).toHaveBeenCalledTimes(1); + }); + + it('defaults to outline appearance', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + + expect(element.appearance).toBe(SearchInputAppearance.outline); + }); + + it('supports super-ghost appearance', async () => { + ({ element, connect, disconnect } = await setup()); + element.appearance = SearchInputAppearance.superGhost; + await connect(); + await waitForUpdatesAsync(); + + expect(element.appearance).toBe(SearchInputAppearance.superGhost); + }); +}); \ No newline at end of file diff --git a/packages/ok-components/src/search-input/types.ts b/packages/ok-components/src/search-input/types.ts new file mode 100644 index 0000000000..af9460037b --- /dev/null +++ b/packages/ok-components/src/search-input/types.ts @@ -0,0 +1,8 @@ +export const SearchInputAppearance = { + block: 'block', + outline: 'outline', + ghost: 'ghost', + superGhost: 'super-ghost' +} as const; + +export type SearchInputAppearance = (typeof SearchInputAppearance)[keyof typeof SearchInputAppearance]; \ No newline at end of file diff --git a/packages/storybook/src/ok/search-input/search-input-matrix.stories.ts b/packages/storybook/src/ok/search-input/search-input-matrix.stories.ts new file mode 100644 index 0000000000..1c4c92481e --- /dev/null +++ b/packages/storybook/src/ok/search-input/search-input-matrix.stories.ts @@ -0,0 +1,97 @@ +import type { Meta, StoryFn } from '@storybook/html-vite'; +import { html, ViewTemplate } from '@ni/fast-element'; +import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers'; +import { searchInputTag } from '@ni/ok-components/dist/esm/search-input'; +import { SearchInputAppearance } from '@ni/ok-components/dist/esm/search-input/types'; +import { + createMatrixInteractionsFromStates, + createMatrixThemeStory, + sharedMatrixParameters +} from '../../utilities/matrix'; + +const metadata: Meta = { + title: 'Tests Ok/Search Input', + parameters: { + ...sharedMatrixParameters() + } +}; + +export default metadata; + +type SearchInputMatrixState = readonly [string, keyof typeof SearchInputAppearance]; + +const searchInputStates: SearchInputMatrixState[] = [ + ['block', 'block'], + ['outline', 'outline'], + ['ghost', 'ghost'], + ['super-ghost', 'superGhost'] +]; + +const searchField = ( + label: string, + appearance: keyof typeof SearchInputAppearance +): ViewTemplate => html` +
+
${() => label}
+ <${searchInputTag} + appearance="${() => SearchInputAppearance[appearance]}" + placeholder="Search..." + style="width: 124px; --ok-search-input-height: 32px;" + > +
+`; + +export const statesThemeMatrix: StoryFn = createMatrixThemeStory(html` +
+ ${searchField('Search_Block_Light_32', 'block')} + ${searchField('Search_Outline_Light_32', 'outline')} + ${searchField('Search_Ghost_Light_32', 'ghost')} + ${searchField('Search_SuperGhost_Light_32', 'superGhost')} +
+`); + +export const interactionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates( + (label: string, appearance: keyof typeof SearchInputAppearance): ViewTemplate => html` +
+
${() => label}
+ <${searchInputTag} + appearance="${() => SearchInputAppearance[appearance]}" + placeholder="Search..." + style="width: 124px; --ok-search-input-height: 32px;" + > +
+ `, + { + hover: searchInputStates, + hoverActive: [], + active: [], + focus: searchInputStates + } + ) +); + +export const typedThemeMatrix: StoryFn = createMatrixThemeStory(html` +
+ ${searchField('Typed_Block_Light_32', 'block')} + ${searchField('Typed_Outline_Light_32', 'outline')} + ${searchField('Typed_Ghost_Light_32', 'ghost')} + ${searchField('Typed_SuperGhost_Light_32', 'superGhost')} +
+`); + +typedThemeMatrix.play = async ({ step }): Promise => { + const searchInputs = Array.from(document.querySelectorAll(searchInputTag)); + + await step('Type search text into each input', async () => { + await Promise.all(searchInputs.map(async searchInput => { + const input = searchInput.shadowRoot?.querySelector('input'); + + if (input) { + input.value = 'Search'; + input.dispatchEvent(new Event('input', { bubbles: true, composed: true })); + await waitForUpdatesAsync(); + } + })); + }); +}; \ No newline at end of file diff --git a/packages/storybook/src/ok/search-input/search-input.mdx b/packages/storybook/src/ok/search-input/search-input.mdx new file mode 100644 index 0000000000..bf62e0c892 --- /dev/null +++ b/packages/storybook/src/ok/search-input/search-input.mdx @@ -0,0 +1,15 @@ +import { Canvas, Controls, Meta, Title } from '@storybook/addon-docs/blocks'; +import * as searchInputStories from './search-input.stories'; + + + + +A search field with a leading magnifying glass icon and a clear affordance when a value is present. + +It supports `block`, `outline`, `ghost`, and `super-ghost` appearances. Height can be adjusted by client CSS via `--ok-search-input-height` rather than separate built-in size variants. + +<Canvas of={searchInputStories.defaultStory} /> + +## API + +<Controls of={searchInputStories.defaultStory} /> \ No newline at end of file diff --git a/packages/storybook/src/ok/search-input/search-input.stories.ts b/packages/storybook/src/ok/search-input/search-input.stories.ts new file mode 100644 index 0000000000..570f0f904d --- /dev/null +++ b/packages/storybook/src/ok/search-input/search-input.stories.ts @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from '@storybook/html-vite'; +import { html } from '@ni/fast-element'; +import { fn } from 'storybook/test'; +import { searchInputTag } from '@ni/ok-components/dist/esm/search-input'; +import { SearchInputAppearance } from '@ni/ok-components/dist/esm/search-input/types'; +import { + appearanceDescription, + apiCategory, + createUserSelectedThemeStory, + placeholderDescription +} from '../../utilities/storybook'; + +interface SearchInputArgs { + appearance: keyof typeof SearchInputAppearance; + placeholder: string; + initialSearchValue: string; + input: (e: Event) => void; + change: (e: Event) => void; +} + +const metadata: Meta<SearchInputArgs> = { + title: 'Ok/Search Input', + render: createUserSelectedThemeStory(html<SearchInputArgs>` + <div style="width: 320px; padding: 16px;"> + <${searchInputTag} + appearance="${x => SearchInputAppearance[x.appearance]}" + placeholder="${x => x.placeholder}" + value="${x => x.initialSearchValue}" + @input="${(x, c) => x.input(c.event)}" + @change="${(x, c) => x.change(c.event)}" + ></${searchInputTag}> + </div> + `), + argTypes: { + appearance: { + description: appearanceDescription({ componentName: 'search input' }), + options: Object.keys(SearchInputAppearance), + control: { type: 'radio' }, + table: { category: apiCategory.attributes } + }, + placeholder: { + description: placeholderDescription({ componentName: 'search input' }), + table: { category: apiCategory.attributes } + }, + initialSearchValue: { + description: 'Initial text rendered in the search input.', + table: { category: apiCategory.nonAttributeProperties } + }, + input: { + table: { category: apiCategory.events }, + control: false + }, + change: { + table: { category: apiCategory.events }, + control: false + } + }, + args: { + appearance: 'outline', + placeholder: 'Search', + initialSearchValue: '', + input: fn(), + change: fn() + } +}; + +export default metadata; + +export const defaultStory: StoryObj<SearchInputArgs> = { + name: 'default' +}; \ No newline at end of file From 78d67c4f685232f26ccdfd719b593262e6798900 Mon Sep 17 00:00:00 2001 From: Fred Visser <1458528+fredvisser@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:48:44 -0500 Subject: [PATCH 2/2] Remove storybook/test from search-input story --- .../src/ok/search-input/search-input.stories.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/storybook/src/ok/search-input/search-input.stories.ts b/packages/storybook/src/ok/search-input/search-input.stories.ts index 570f0f904d..3a718c0ed6 100644 --- a/packages/storybook/src/ok/search-input/search-input.stories.ts +++ b/packages/storybook/src/ok/search-input/search-input.stories.ts @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from '@storybook/html-vite'; import { html } from '@ni/fast-element'; -import { fn } from 'storybook/test'; import { searchInputTag } from '@ni/ok-components/dist/esm/search-input'; import { SearchInputAppearance } from '@ni/ok-components/dist/esm/search-input/types'; import { @@ -14,8 +13,8 @@ interface SearchInputArgs { appearance: keyof typeof SearchInputAppearance; placeholder: string; initialSearchValue: string; - input: (e: Event) => void; - change: (e: Event) => void; + input?: (e: Event) => void; + change?: (e: Event) => void; } const metadata: Meta<SearchInputArgs> = { @@ -26,8 +25,6 @@ const metadata: Meta<SearchInputArgs> = { appearance="${x => SearchInputAppearance[x.appearance]}" placeholder="${x => x.placeholder}" value="${x => x.initialSearchValue}" - @input="${(x, c) => x.input(c.event)}" - @change="${(x, c) => x.change(c.event)}" ></${searchInputTag}> </div> `), @@ -58,9 +55,7 @@ const metadata: Meta<SearchInputArgs> = { args: { appearance: 'outline', placeholder: 'Search', - initialSearchValue: '', - input: fn(), - change: fn() + initialSearchValue: '' } };