diff --git a/packages/main/cypress/specs/TabularInput.cy.tsx b/packages/main/cypress/specs/TabularInput.cy.tsx new file mode 100644 index 000000000000..9e75476aff9a --- /dev/null +++ b/packages/main/cypress/specs/TabularInput.cy.tsx @@ -0,0 +1,660 @@ +import TabularInput from "../../src/TabularInput.js"; +import TableHeaderCell from "../../src/TableHeaderCell.js"; +import TableRow from "../../src/TableRow.js"; +import TableCell from "../../src/TableCell.js"; +import type ResponsivePopover from "../../src/ResponsivePopover.js"; + +describe("TabularInput - Basic Rendering", () => { + it("renders with tabular suggestions", () => { + cy.mount( + + Name + Country + + John + USA + + + Jane + UK + + + ); + + cy.get("[ui5-tabular-input]").should("exist"); + cy.get("[ui5-tabular-input]").find("[ui5-table-header-cell]").should("have.length", 2); + cy.get("[ui5-tabular-input]").find("[ui5-table-row]").should("have.length", 2); + }); + + it("opens suggestions popover on focus and type", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("have.attr", "open"); + }); + + it("closes suggestions popover on Escape", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("have.attr", "open"); + + cy.realPress("Escape"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.have.attr", "open"); + }); +}); + +describe("TabularInput - Highlighting", () => { + it("highlights matching text in cells", () => { + cy.mount( + + Name + + John Smith + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("john"); + + cy.get("@input") + .shadow() + .find("[ui5-table-cell]") + .first() + .find("b") + .should("exist") + .and("have.text", "John"); + }); +}); + +describe("TabularInput - Keyboard Navigation", () => { + it("navigates through rows with Arrow Down/Up", () => { + cy.mount( + + Name + + John + + + Jane + + + Jack + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.realPress("ArrowDown"); + cy.get("@input").should("have.value", "John"); + + cy.realPress("ArrowDown"); + cy.get("@input").should("have.value", "Jane"); + + cy.realPress("ArrowDown"); + cy.get("@input").should("have.value", "Jack"); + + cy.realPress("ArrowUp"); + cy.get("@input").should("have.value", "Jane"); + }); + + it("selects text during navigation", () => { + cy.mount( + + Name + + John + + + Jane + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.realPress("ArrowDown"); + + cy.get("@input").should("have.value", "John"); + + cy.window().then(win => { + const selection = win.getSelection()?.toString(); + expect(selection).to.contain("ohn"); + }); + }); + + it("restores typed value when pressing Arrow Up from first row", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("jo"); + + cy.realPress("ArrowDown"); + cy.get("@input").should("have.value", "John"); + + cy.realPress("ArrowUp"); + cy.get("@input").should("have.value", "jo"); + }); + + it("selects row with Enter key", () => { + const onSelectionChange = cy.spy().as("onSelectionChange"); + + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.realPress("ArrowDown"); + cy.realPress("Enter"); + + cy.get("@input").should("have.value", "John"); + cy.get("@onSelectionChange").should("have.been.called"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.have.attr", "open"); + }); +}); + +describe("TabularInput - Row Selection", () => { + it("selects row on click", () => { + const onSelectionChange = cy.spy().as("onSelectionChange"); + + cy.mount( + + Name + + John + + + Jane + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.get("@input") + .shadow() + .find("[ui5-table-row]") + .eq(1) + .realClick(); + + cy.get("@input").should("have.value", "Jane"); + cy.get("@onSelectionChange").should("have.been.called"); + }); + + it("fires selection-change event during navigation", () => { + const onSelectionChange = cy.spy().as("onSelectionChange"); + + cy.mount( + + Name + + John + + + Jane + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.realPress("ArrowDown"); + cy.get("@onSelectionChange").should("have.been.called"); + }); +}); + +describe("TabularInput - Typeahead", () => { + it("performs typeahead with first matching row", () => { + cy.mount( + + Name + + John + + + Jane + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("jo"); + + cy.get("@input").should("have.value", "john"); + + cy.window().then(win => { + const selection = win.getSelection()?.toString(); + expect(selection).to.contain("hn"); + }); + }); + + it("disables typeahead with noTypeahead property", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("jo"); + + cy.get("@input").should("have.value", "jo"); + }); +}); + +describe("TabularInput - Clear Icon", () => { + it("shows clear icon when value is present", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("test"); + + cy.get("@input") + .shadow() + .find(".ui5-input-clear-icon-wrapper") + .should("exist"); + }); + + it("clears value when clicking clear icon", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("test"); + + cy.get("@input") + .shadow() + .find(".ui5-input-clear-icon-wrapper") + .realClick(); + + cy.get("@input").should("have.value", ""); + }); +}); + +describe("TabularInput - Overflow Mode", () => { + it("renders with Popin overflow mode by default", () => { + cy.mount( + + Col1 + Col2 + + Cell1 + Cell2 + + + ); + + cy.get("[ui5-tabular-input]") + .should("have.attr", "overflow-mode", "Popin"); + }); + + it("accepts Scroll overflow mode", () => { + cy.mount( + + Col1 + Col2 + + Cell1 + Cell2 + + + ); + + cy.get("[ui5-tabular-input]") + .should("have.attr", "overflow-mode", "Scroll"); + }); +}); + +describe("TabularInput - Value State", () => { + it("displays value state", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .should("have.attr", "value-state", "Negative"); + }); + + it("shows value state header in suggestions popover", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("have.attr", "open"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover] [slot='header']") + .should("exist") + .find("[ui5-icon]") + .should("exist"); + }); + + it("shows custom value state message from slot", () => { + cy.mount( + + Name + + John + +
Custom info message
+
+ ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover] [slot='header'] slot[name='valueStateMessage']") + .should("exist"); + }); + + it("shows standalone value state popover when focused without typing", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.have.attr", "open"); + + cy.get("@input") + .shadow() + .find("[ui5-popover].ui5-valuestatemessage-popover") + .should("have.attr", "open"); + }); +}); + +describe("TabularInput - showSuggestions Property", () => { + it("does not open suggestions popover when showSuggestions is false", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.have.attr", "open"); + }); + + it("does not open suggestions popover when showSuggestions is not set (defaults to false)", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.have.attr", "open"); + }); + + it("opens suggestions popover when showSuggestions is true", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("j"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("have.attr", "open"); + }); + + it("does not perform typeahead when showSuggestions is false", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input").realType("jo"); + + cy.get("@input").should("have.value", "jo"); + }); + + it("shows value state popover when showSuggestions is false and has value state", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input") + .shadow() + .find("[ui5-popover].ui5-valuestatemessage-popover") + .should("have.attr", "open"); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.have.attr", "open"); + }); +}); + +describe("TabularInput - Disabled and Readonly", () => { + it("does not open popover when disabled", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick({ force: true }); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.have.attr", "open"); + }); + + it("does not open popover when readonly", () => { + cy.mount( + + Name + + John + + + ); + + cy.get("[ui5-tabular-input]") + .as("input") + .realClick(); + + cy.get("@input") + .shadow() + .find("[ui5-responsive-popover]") + .should("not.have.attr", "open"); + }); +}); diff --git a/packages/main/src/InputTemplate.tsx b/packages/main/src/InputTemplate.tsx index 4a6ce63f14fc..2aa8c2a7eac1 100644 --- a/packages/main/src/InputTemplate.tsx +++ b/packages/main/src/InputTemplate.tsx @@ -6,11 +6,12 @@ import InputPopoverTemplate from "./InputPopoverTemplate.js"; type TemplateHook = () => JsxTemplateResult; -export default function InputTemplate(this: Input, hooks?: { preContent: TemplateHook, postContent: TemplateHook, suggestionsList?: TemplateHook, mobileHeader?: TemplateHook }) { +export default function InputTemplate(this: Input, hooks?: { preContent: TemplateHook, postContent: TemplateHook, suggestionsList?: TemplateHook, mobileHeader?: TemplateHook, popoverTemplate?: TemplateHook }) { const suggestionsList = hooks?.suggestionsList; const mobileHeader = hooks?.mobileHeader; const preContent = hooks?.preContent || defaultPreContent; const postContent = hooks?.postContent || defaultPostContent; + const popoverTemplate = hooks?.popoverTemplate; return ( <> @@ -120,7 +121,7 @@ export default function InputTemplate(this: Input, hooks?: { preContent: Templat - { InputPopoverTemplate.call(this, { suggestionsList, mobileHeader }) } + { popoverTemplate ? popoverTemplate.call(this) : InputPopoverTemplate.call(this, { suggestionsList, mobileHeader }) } ); } diff --git a/packages/main/src/TabularInput.ts b/packages/main/src/TabularInput.ts new file mode 100644 index 000000000000..28b26686a106 --- /dev/null +++ b/packages/main/src/TabularInput.ts @@ -0,0 +1,717 @@ +import type UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import { isPhone, isAndroid } from "@ui5/webcomponents-base/dist/Device.js"; +import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; +import generateHighlightedMarkup from "@ui5/webcomponents-base/dist/util/generateHighlightedMarkupFirstMatch.js"; + +import Input from "./Input.js"; +import type { IInputSuggestionItem } from "./Input.js"; +import type Table from "./Table.js"; +import type TableHeaderCell from "./TableHeaderCell.js"; +import type TableCell from "./TableCell.js"; +import type TableRow from "./TableRow.js"; +import type ResponsivePopover from "./ResponsivePopover.js"; +import type TableOverflowMode from "./types/TableOverflowMode.js"; + +import TabularInputTemplate from "./TabularInputTemplate.js"; +import tabularInputStyles from "./generated/themes/TabularInput.css.js"; +import SuggestionsCss from "./generated/themes/Suggestions.css.js"; + +import { + INPUT_SUGGESTIONS, + INPUT_SUGGESTIONS_MORE_HITS, + LIST_ITEM_POSITION, +} from "./generated/i18n/i18n-defaults.js"; + +/** + * Represents highlighted cell content for a row + * @private + */ +type HighlightedCellContent = { + text: string; + highlightedMarkup: string; +}; + +/** + * Represents a processed suggestion row with highlighted content + * @private + */ +type ProcessedSuggestionRow = { + row: ITabularSuggestionRow; + cells: HighlightedCellContent[]; +}; + +/** + * Interface for tabular suggestion row items + * @public + */ +interface ITabularSuggestionRow extends UI5Element { + cells: TableCell[]; + selected?: boolean; + focused?: boolean; +} + +type TabularInputSelectionChangeEventDetail = { + row: ITabularSuggestionRow | null; +} + +/** + * @class + * ### Overview + * + * The `ui5-tabular-input` component is an input field with tabular suggestions support. + * It displays suggestions in a table format with multiple columns, allowing users to + * see more information about each suggestion before selecting it. + * + * Similar to the OpenUI5 sap.m.Input with tabular suggestions, this component supports: + * - Multiple columns via `suggestionColumns` slot + * - Tabular rows via `suggestionRows` slot + * - Automatic popin mode for responsive behavior + * + * ### Usage + * + * Use this component when: + * - Users need to see additional information in columns for each suggestion + * - A simple text-based suggestion list is not sufficient + * - You want to display data in a tabular format + * + * ### Difference from ui5-input + * + * This component uses its own tabular suggestion mechanism instead of the standard + * `showSuggestions` / `suggestionItems` from ui5-input. The tabular suggestions + * are defined via: + * - `suggestionColumns`: Table header cells defining the columns + * - `suggestionRows`: Table rows with cells containing the suggestion data + * + * ### Keyboard Handling + * + * The component inherits keyboard handling from `ui5-input`: + * - [Down] - Navigates to the next suggestion row + * - [Up] - Navigates to the previous suggestion row + * - [Enter] - Selects the focused suggestion row + * - [Escape] - Closes the suggestion popover + * + * ### ES6 Module Import + * + * `import "@ui5/webcomponents/dist/TabularInput.js";` + * + * @constructor + * @extends Input + * @public + * @experimental + */ +@customElement({ + tag: "ui5-tabular-input", + languageAware: true, + formAssociated: true, + renderer: jsxRenderer, + template: TabularInputTemplate, + styles: [Input.styles, SuggestionsCss, tabularInputStyles], +}) + +class TabularInput extends Input { + // @ts-expect-error - Intentionally override selection-change to use 'row' instead of 'item' + eventDetails!: Omit & { + "selection-change": TabularInputSelectionChangeEventDetail, + } + + /** + * Defines the columns for the tabular suggestions. + * Use `ui5-table-header-cell` components to define the column headers. + * + * **Note:** The columns define the structure of the suggestion table header. + * Each column can have properties like `width`, `minWidth`, `importance` (for popin), + * and `popinText`. + * + * @public + */ + @slot({ type: HTMLElement }) + suggestionColumns!: Slot; + + /** + * Defines the rows for the tabular suggestions. + * Use `ui5-table-row` components with `ui5-table-cell` children to define each suggestion row. + * + * **Note:** The cells in each row should correspond to the columns defined in `suggestionColumns`. + * + * @public + */ + @slot({ type: HTMLElement }) + suggestionRows!: Slot; + + /** + * Defines the overflow behavior of the suggestion table. + * + * **Note:** When set to `Popin`, columns that don't fit will be shown as pop-in content. + * When set to `Scroll`, a horizontal scrollbar will appear. + * + * @default "Popin" + * @public + */ + @property() + overflowMode: `${TableOverflowMode}` = "Popin"; + + /** + * Internal property to track if table suggestions are being used + * @private + */ + @property({ type: Boolean, noAttribute: true }) + _useTabularSuggestions = false; + + /** + * Internal property reflecting whether a suggestion row has focus. + * Used by CSS to hide input focus outline during row navigation. + * @private + */ + @property({ type: Boolean }) + _rowFocused = false; + + /** + * Stores processed rows with highlighted cell content + * @private + */ + _processedRows: ProcessedSuggestionRow[] = []; + + /** + * Stores the matched row for typeahead (similar to Input's _matchedSuggestionItem) + * @private + */ + _matchedTabularRow?: ITabularSuggestionRow; + + get _effectiveShowSuggestions() { + if (this._useTabularSuggestions) { + return this.showSuggestions; + } + return super._effectiveShowSuggestions; + } + + /** + * Override: Return tabular rows as suggestion items for the parent's hasItems check + * Returns empty when showSuggestions is false to prevent parent from opening popover + */ + get _flattenItems(): Array { + if (this._useTabularSuggestions) { + return this.showSuggestions ? this.suggestionRows as unknown as Array : []; + } + return super._flattenItems; + } + + onBeforeRendering() { + this._useTabularSuggestions = this.suggestionColumns.length > 0 && this.suggestionRows.length > 0; + + if (this._useTabularSuggestions) { + this._processRows(); + this._handleTabularPopoverOpen(); + this._handleTabularTypeAhead(); + } + + super.onBeforeRendering(); + } + + /** + * @private + */ + _handleTabularPopoverOpen() { + if (!this._effectiveShowSuggestions) { + return; + } + + const hasItems = this._visibleRows.length > 0; + const hasValue = !!this.value; + const isFocused = this.shadowRoot?.querySelector("input") === getActiveElement(); + const preventOpenPicker = this.disabled || this.readonly; + + if (preventOpenPicker) { + this.open = false; + } else if (!this._isPhone) { + this.open = hasItems && (this.open || (hasValue && isFocused && this.isTyping)); + } + } + + /** + * @private + */ + _handleTabularTypeAhead() { + if (!this._effectiveShowSuggestions) { + return; + } + + const innerInput = this.getInputDOMRefSync(); + if (!innerInput || !this.value) { + return; + } + + const autoCompletedChars = innerInput.selectionEnd! - innerInput.selectionStart!; + + if (this._shouldAutocomplete && !isAndroid() && !autoCompletedChars && !this._isKeyNavigation) { + const matchingRow = this._getFirstMatchingRow(this.value); + if (matchingRow) { + if (!this._isComposing) { + this._performRowTypeAhead(matchingRow); + } + this._selectMatchingRow(matchingRow); + } else { + this._matchedTabularRow = undefined; + } + } + } + + /** + * @private + */ + _getFirstMatchingRow(current: string): ITabularSuggestionRow | undefined { + const visibleRows = this._visibleRows; + if (!visibleRows.length) { + return; + } + + const currentLower = current.toLowerCase(); + + return visibleRows.find(row => { + const firstCellText = this._getRowValue(row).toLowerCase(); + return firstCellText.startsWith(currentLower); + }); + } + + /** + * @private + */ + _performRowTypeAhead(row: ITabularSuggestionRow) { + const suggestionText = this._getRowValue(row); + const typedValue = this.typedInValue; + + if (suggestionText.toLowerCase().startsWith(typedValue.toLowerCase())) { + this.value = typedValue + suggestionText.substring(typedValue.length); + } + + this._performTextSelection = true; + this._shouldAutocomplete = false; + } + + /** + * @private + */ + _selectMatchingRow(row: ITabularSuggestionRow) { + this._deselectAllRows(); + + row.selected = true; + this._matchedTabularRow = row; + + this.fireDecoratorEvent("selection-change", { + row, + }); + } + + onAfterRendering() { + if (this._useTabularSuggestions) { + if (this._performTextSelection) { + if (this.typedInValue.length && this.value.length) { + this._adjustSelectionRange(); + } + this.fireDecoratorEvent("type-ahead"); + } + this._performTextSelection = false; + + return; + } + + super.onAfterRendering(); + } + + /** + * @private + */ + _adjustSelectionRange() { + if (this._useTabularSuggestions) { + const innerInput = this.getInputDOMRefSync(); + if (innerInput) { + innerInput.setSelectionRange(this.typedInValue.length, this.value.length); + } + return; + } + super._adjustSelectionRange(); + } + + /** + * Processes rows and generates highlighted markup for cell content. + * @private + */ + _processRows() { + const typedValue = this.typedInValue; + this._processedRows = []; + + this.suggestionRows.forEach(row => { + const cells = row.cells || []; + const processedCells: HighlightedCellContent[] = cells.map(cell => { + const cellText = cell.textContent?.trim() || ""; + + return { + text: cellText, + highlightedMarkup: generateHighlightedMarkup(cellText, typedValue), + }; + }); + + this._processedRows.push({ + row, + cells: processedCells, + }); + }); + } + + get _visibleRows(): ITabularSuggestionRow[] { + return this.suggestionRows.filter(row => !(row as UI5Element).hidden); + } + + get _visibleProcessedRows(): ProcessedSuggestionRow[] { + const visibleRowSet = new Set(this._visibleRows); + return this._processedRows.filter(pr => visibleRowSet.has(pr.row)); + } + + /** + * @private + */ + _onSuggestionRowClick(row: ITabularSuggestionRow) { + this._selectRow(row, false); + } + + /** + * Handles row-click event from the table to select the corresponding suggestion. + * @private + */ + _onTableRowClick(e: CustomEvent<{ row: TableRow }>) { + const clickedRow = e.detail.row; + const rowIndex = parseInt(clickedRow.dataset.rowIndex || "0"); + const suggestionRow = this._visibleRows[rowIndex]; + + if (suggestionRow) { + this._selectRow(suggestionRow, false); + } + } + + /** + * @private + */ + _selectRow(row: ITabularSuggestionRow, keyboardUsed: boolean) { + const rowValue = this._getRowValue(row); + + this.value = rowValue; + this.typedInValue = rowValue; + this.open = false; + + this.fireDecoratorEvent("selection-change", { + row, + }); + this.fireDecoratorEvent("change"); + this.fireDecoratorEvent("input", { inputType: "" }); + + this._deselectAllRows(); + row.selected = true; + this._matchedTabularRow = undefined; + this._rowFocused = false; + this.isTyping = false; + + if (!keyboardUsed && !isPhone()) { + this.focus(); + } + } + + /** + * @private + */ + _getRowValue(row: ITabularSuggestionRow): string { + const cells = row.cells || []; + + if (cells.length > 0) { + return cells[0].textContent?.trim() || ""; + } + + return ""; + } + + /** + * @private + */ + _deselectAllRows() { + this.suggestionRows.forEach(row => { + row.selected = false; + row.focused = false; + }); + } + + /** + * @private + */ + _handleDown(e: KeyboardEvent) { + if (this._useTabularSuggestions && this.open) { + e.preventDefault(); + this._navigateRows(true); + return; + } + super._handleDown(e); + } + + /** + * @private + */ + _handleUp(e: KeyboardEvent) { + if (this._useTabularSuggestions && this.open) { + e.preventDefault(); + this._navigateRows(false); + return; + } + super._handleUp(e); + } + + /** + * @private + */ + _navigateRows(forward: boolean) { + const visibleRows = this._visibleRows; + + if (visibleRows.length === 0) { + return; + } + + const currentIndex = visibleRows.findIndex(row => row.focused || row.selected); + + let nextIndex: number; + if (forward) { + if (currentIndex >= visibleRows.length - 1) { + return; + } + nextIndex = currentIndex < 0 ? 0 : currentIndex + 1; + } else { + if (currentIndex <= 0) { + this._deselectAllRows(); + this._matchedTabularRow = undefined; + this._rowFocused = false; + this._clearAnnouncement(); + this.value = this.typedInValue; + return; + } + nextIndex = currentIndex - 1; + } + + this._deselectAllRows(); + this._matchedTabularRow = undefined; + + visibleRows[nextIndex].focused = true; + this._rowFocused = true; + + const previewValue = this._getRowValue(visibleRows[nextIndex]); + this.value = previewValue; + this._performTextSelection = true; + + this._announceSelectedRow(nextIndex); + this._scrollRowIntoView(nextIndex); + + this.fireDecoratorEvent("selection-change", { + row: visibleRows[nextIndex], + }); + } + + /** + * @private + */ + _handleEnter(e: KeyboardEvent) { + if (this._useTabularSuggestions) { + const visibleRows = this._visibleRows; + const focusedRow = visibleRows.find(row => row.focused); + const innerInput = this.getInputDOMRefSync()!; + + let rowToSelect = focusedRow || this._matchedTabularRow; + + if (!rowToSelect) { + rowToSelect = visibleRows.find(row => { + return this._getRowValue(row).toLowerCase() === this.value.toLowerCase(); + }); + } + + if (rowToSelect) { + const rowValue = this._getRowValue(rowToSelect); + innerInput.setSelectionRange(rowValue.length, rowValue.length); + + if (this.open) { + e.preventDefault(); + } + this._selectRow(rowToSelect, true); + return; + } + + if (this.open) { + this.open = false; + } + this.lastConfirmedValue = this.value; + return; + } + super._handleEnter(e); + } + + /** + * @private + */ + _handleEscape() { + if (this._useTabularSuggestions && this.open) { + this.value = this.typedInValue || this.valueBeforeSelectionStart; + this.open = false; + this._deselectAllRows(); + this._matchedTabularRow = undefined; + this._rowFocused = false; + this.isTyping = false; + this._clearAnnouncement(); + return; + } + super._handleEscape(); + } + + /** + * @private + */ + _clearPopoverFocusAndSelection() { + if (this._useTabularSuggestions) { + this._deselectAllRows(); + this.hasSuggestionItemSelected = false; + return; + } + super._clearPopoverFocusAndSelection(); + } + + get _hasTabularSuggestions(): boolean { + return this._useTabularSuggestions && this._visibleRows.length > 0; + } + + get _columnsCount(): number { + return this.suggestionColumns.length; + } + + get _isRowFocused(): boolean { + return this._useTabularSuggestions && this._visibleRows.some(row => row.focused); + } + + override get _isSuggestionsFocused(): boolean { + if (this._useTabularSuggestions) { + return this._isRowFocused; + } + return super._isSuggestionsFocused || false; + } + + /** + * Returns the accessible name for the suggestions popover + * @private + */ + get _tabularSuggestionsAccessibleName(): string { + return Input.i18nBundle.getText(INPUT_SUGGESTIONS); + } + + /** + * Returns the count text for available suggestions + * @private + */ + get _tabularSuggestionsCountText(): string { + return Input.i18nBundle.getText(INPUT_SUGGESTIONS_MORE_HITS, this._visibleRows.length); + } + + /** + * Announces the currently selected row for screen readers using a live region. + * @private + */ + _announceSelectedRow(rowIndex: number) { + const invisibleText = this.shadowRoot?.querySelector("#selectionText"); + if (invisibleText) { + const rowText = this._getRowValue(this._visibleRows[rowIndex]); + const positionText = Input.i18nBundle.getText(LIST_ITEM_POSITION, rowIndex + 1, this._visibleRows.length); + invisibleText.textContent = `${rowText} ${positionText}`; + } + } + + /** + * Clears the announcement text when closing the popover. + * @private + */ + _clearAnnouncement() { + const invisibleText = this.shadowRoot?.querySelector("#selectionText"); + if (invisibleText) { + invisibleText.textContent = ""; + } + } + + /** + * Returns the tabular suggestions popover element + * @private + */ + _getTabularPopover() { + return this.shadowRoot?.querySelector(".ui5-suggestions-popover"); + } + + /** + * Scrolls the row at the given index into view within the suggestions popover. + * @private + */ + _scrollRowIntoView(rowIndex: number) { + const popover = this._getTabularPopover(); + if (!popover) { + return; + } + + const table = popover.querySelector("[ui5-table]"); + const rowElement = table?.rows[rowIndex]; + + if (!rowElement) { + return; + } + + const scrollContainer = popover.querySelector(".ui5-tabular-input-suggestions-wrapper"); + if (!scrollContainer) { + return; + } + + const containerRect = scrollContainer.getBoundingClientRect(); + const rowRect = rowElement.getBoundingClientRect(); + + const isRowAboveView = rowRect.top < containerRect.top; + const isRowBelowView = rowRect.bottom > containerRect.bottom; + + if (isRowAboveView || isRowBelowView) { + rowElement.scrollIntoView({ + behavior: "auto", + block: "nearest", + }); + } + } + + /** + * Override focusout handler to prevent closing popover when clicking inside it + * @private + */ + _onfocusout(e: FocusEvent) { + if (this._useTabularSuggestions) { + const toBeFocused = e.relatedTarget as HTMLElement; + const popover = this._getTabularPopover(); + + if (popover?.contains(toBeFocused) || this.contains(toBeFocused)) { + return; + } + + this.focused = false; + this.open = false; + this.isTyping = false; + this.lastConfirmedValue = ""; + this._clearPopoverFocusAndSelection(); + return; + } + + super._onfocusout(e); + } +} + +TabularInput.define(); + +export default TabularInput; +export type { + ITabularSuggestionRow, + TabularInputSelectionChangeEventDetail, +}; diff --git a/packages/main/src/TabularInputPopoverTemplate.tsx b/packages/main/src/TabularInputPopoverTemplate.tsx new file mode 100644 index 000000000000..4dcf6b4b339c --- /dev/null +++ b/packages/main/src/TabularInputPopoverTemplate.tsx @@ -0,0 +1,193 @@ +import type TabularInput from "./TabularInput.js"; +import type { JsxTemplateResult } from "@ui5/webcomponents-base/dist/index.js"; + +import Icon from "./Icon.js"; +import error from "@ui5/webcomponents-icons/dist/error.js"; +import alert from "@ui5/webcomponents-icons/dist/alert.js"; +import sysEnter2 from "@ui5/webcomponents-icons/dist/sys-enter-2.js"; +import information from "@ui5/webcomponents-icons/dist/information.js"; + +import PopoverHorizontalAlign from "./types/PopoverHorizontalAlign.js"; +import Popover from "./Popover.js"; +import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; +import ResponsivePopover from "./ResponsivePopover.js"; +import Button from "./Button.js"; +import Title from "./Title.js"; +import Input from "./Input.js"; +import Table from "./Table.js"; +import TableHeaderRow from "./TableHeaderRow.js"; +import TableHeaderCell from "./TableHeaderCell.js"; +import TableRow from "./TableRow.js"; +import TableCell from "./TableCell.js"; + +export default function TabularInputPopoverTemplate(this: TabularInput): JsxTemplateResult { + return ( + <> + + {this._isPhone && +
+
+ + {this._headerTitleText} + +
+
+
+ +
+
+ {this.hasValueStateMessage && +
+ + {this.open && valueStateMessage.call(this)} +
+ } +
+ } + + {!this._isPhone && this.hasValueStateMessage && +
+ + {this.open && valueStateMessage.call(this)} +
+ } + + {tabularSuggestionsList.call(this)} + + {this._isPhone && + + } +
+ + {this.hasValueStateMessage && ( + +
+ + {this.valueStateOpen && valueStateMessage.call(this)} +
+
+ )} + + ); +} + +function valueStateMessage(this: TabularInput) { + return ( + <> + { + this.shouldDisplayDefaultValueStateMessage ? this.valueStateText : + } + + ); +} + +function valueStateMessageInputIcon(this: TabularInput) { + const iconPerValueState = { + Negative: error, + Critical: alert, + Positive: sysEnter2, + Information: information, + }; + + return this.valueState !== ValueState.None ? iconPerValueState[this.valueState as keyof typeof iconPerValueState] : ""; +} + +function tabularSuggestionsList(this: TabularInput): JsxTemplateResult { + const isScrollMode = this.overflowMode === "Scroll"; + const lastColumnIndex = this.suggestionColumns.length - 1; + + return ( +
+
+ + {this.suggestionColumns.map((col, index) => { + const width = (isScrollMode && index === lastColumnIndex) ? undefined : col.width; + return ( + + {col.textContent} + + ); + })} + + + {this._visibleProcessedRows.map((processedRow, rowIndex) => ( + + {processedRow.cells.map((cell, cellIndex) => ( + + + ))} + + ))} +
+ + ); +} diff --git a/packages/main/src/TabularInputTemplate.tsx b/packages/main/src/TabularInputTemplate.tsx new file mode 100644 index 000000000000..7744e82895e9 --- /dev/null +++ b/packages/main/src/TabularInputTemplate.tsx @@ -0,0 +1,23 @@ +import InputTemplate from "./InputTemplate.js"; +import type Input from "./Input.js"; +import type TabularInput from "./TabularInput.js"; +import TabularInputPopoverTemplate from "./TabularInputPopoverTemplate.js"; + +export default function TabularInputTemplate(this: TabularInput) { + return InputTemplate.call(this as unknown as Input, { + preContent: tabularPreContent.bind(this), + postContent: tabularPostContent.bind(this), + popoverTemplate: tabularPopoverTemplate.bind(this), + }); +} + +function tabularPreContent(this: TabularInput) {} +function tabularPostContent(this: TabularInput) {} + +function tabularPopoverTemplate(this: TabularInput) { + if (!this._useTabularSuggestions) { + return; + } + + return TabularInputPopoverTemplate.call(this); +} diff --git a/packages/main/src/bundle.esm.ts b/packages/main/src/bundle.esm.ts index d916b641dfac..05bd63285e0e 100644 --- a/packages/main/src/bundle.esm.ts +++ b/packages/main/src/bundle.esm.ts @@ -84,6 +84,7 @@ import Icon from "./Icon.js"; import Input from "./Input.js"; import SuggestionItemCustom from "./SuggestionItemCustom.js"; import MultiInput from "./MultiInput.js"; +import TabularInput from "./TabularInput.js"; import Label from "./Label.js"; import LastOptions from "./dynamic-date-range-options/LastOptions.js"; import Link from "./Link.js"; diff --git a/packages/main/src/themes/TabularInput.css b/packages/main/src/themes/TabularInput.css new file mode 100644 index 000000000000..0be759b2d322 --- /dev/null +++ b/packages/main/src/themes/TabularInput.css @@ -0,0 +1,51 @@ +:host([open][focused][_row-focused]) .ui5-input-focusable-element::after { + content: none; +} + +:host([open][focused][_row-focused]) { + border-color: var(--sapField_BorderColor); + background-color: var(--sapField_Background); +} + +.ui5-tabular-input-suggestions-wrapper { + width: 100%; + max-height: var(--_ui5_tabular_suggestions_max_height, 20rem); + overflow: auto; +} + +.ui5-tabular-suggestions-table { + width: 100%; +} + +.ui5-tabular-suggestions-table::part(before), +.ui5-tabular-suggestions-table::part(after) { + display: none; +} + +.ui5-tabular-suggestions-table [ui5-table-row].ui5-tabular-suggestions-row--focused, +.ui5-tabular-suggestions-table [ui5-table-row].ui5-tabular-suggestions-row--selected { + background-color: var(--sapList_SelectionBackgroundColor); +} + +.ui5-tabular-suggestions-table [ui5-table-row].ui5-tabular-suggestions-row--focused { + outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); + outline-offset: var(--_ui5_tabular_suggestions_focus_offset, -0.125rem); +} + +.ui5-tabular-suggestions-table [ui5-table-row].ui5-tabular-suggestions-row--focused:hover, +.ui5-tabular-suggestions-table [ui5-table-row].ui5-tabular-suggestions-row--selected:hover { + background-color: var(--sapList_Hover_SelectionBackground); +} + +.ui5-tabular-suggestions-table [ui5-table-cell] b { + font-weight: bold; + color: var(--sapList_TextColor); +} + +.ui5-tabular-suggestions-table [ui5-table-row] [ui5-table-cell]:first-of-type { + font-weight: 500; +} + +:host([_isPhone]) .ui5-tabular-input-suggestions-wrapper { + max-height: none; +} diff --git a/packages/main/test/pages/TabularInput.html b/packages/main/test/pages/TabularInput.html new file mode 100644 index 000000000000..804bddf9310d --- /dev/null +++ b/packages/main/test/pages/TabularInput.html @@ -0,0 +1,661 @@ + + + + + + TabularInput - Test Page + + + + + TabularInput Component - POC Test Page + This page demonstrates the TabularInput component with tabular suggestions. + + +
+ Example 1: Product Search +

+ Search for products. The suggestions show Product ID, Name, Category, and Price in columns. + Try typing "Laptop" or "Phone". Matching text is highlighted in the suggestions. +

+ + + + Product ID + Name + Category + Price + + + + PRD-001 + Laptop Pro 15 + Electronics + $1,299 + + + PRD-002 + Smartphone X + Electronics + $899 + + + PRD-003 + Wireless Mouse + Accessories + $49 + + + PRD-004 + USB-C Hub + Accessories + $79 + + + PRD-005 + Phone Case + Accessories + $29 + + + +
Selected: (none)
+
+ + +
+ Example 2: Employee Directory +

+ Search for employees by name. Typed text is highlighted in all cells. +

+ + + + Name + Department + Location + Extension + + + + John Smith + Engineering + Building A + x1234 + + + Jane Doe + Marketing + Building B + x2345 + + + James Wilson + Sales + Building A + x3456 + + + Sarah Johnson + HR + Building C + x4567 + + + Mike Brown + Engineering + Building A + x5678 + + + +
Selected: (none)
+
+ + +
+ Example 3: Popin Overflow Mode (Default) +

+ This example uses overflow-mode="Popin" (the default) in a narrow container. + Columns will stack vertically when there's not enough space. +

+ +
+ + + Code + Description + Quantity + Status + + + + A001 + Widget Alpha + 150 + In Stock + + + B002 + Widget Beta + 75 + Low Stock + + + C003 + Widget Gamma + 0 + Out of Stock + + +
+ +
Selected: (none)
+
+ + +
+ Example 4: Scroll Overflow Mode +

+ This example uses overflow-mode="Scroll" in a narrow container. + A horizontal scrollbar appears when columns don't fit. +

+ +
+ + + Code + Description + Quantity + Status + + + + A001 + Widget Alpha + 150 + In Stock + + + B002 + Widget Beta + 75 + Low Stock + + + C003 + Widget Gamma + 0 + Out of Stock + + +
+ +
Selected: (none)
+
+ + + +
+ Example 6: With Clear Icon +

+ TabularInput with show-clear-icon enabled for easy value clearing. +

+ + + + ID + Task + Priority + + + + T-101 + Review PR #123 + High + + + T-102 + Update documentation + Medium + + + T-103 + Fix login bug + Critical + + + +
Selected: (none)
+
+ + +
+ Example 7: Value States +

+ TabularInput supports all value states: Negative (Error), Critical (Warning), Positive (Success), and Information. +

+ +
+ + Name + Status + + Item A + Invalid + + + + + Name + Status + + Item B + Pending + + + + + Name + Status + + Item C + Approved + + + + + Name + Status + + Item D + Info + + +
+
+ + +
+ Example 8: Value State with Custom Message +

+ Custom value state messages are shown in a popover when the input is focused. + Focus on each input to see the message. +

+ +
+ +
+ This product ID is invalid. Please enter a valid ID starting with "PRD-". +
+ Product + Price + + PRD-001 + $99 + +
+ + +
+ Stock is running low. Consider reordering soon. +
+ Product + Stock + + Widget X + 5 left + +
+ + +
+ Tip: You can use wildcards (*) for broader search results. +
+ Search + Results + + Widget* + 25 matches + +
+
+
+ + +
+ Example 9: Value State with Formatted Text +

+ Value state messages can contain formatted text with bold, italic, and other styling. +

+ +
+ +
+ Invalid Entry: The value "XYZ-999" does not match any record. + Please check your input and try again. +
+ Code + Description + + ABC-001 + Valid Code + +
+ + +
+ Attention: This item will be discontinued on December 31, 2026. + Consider selecting an alternative. +
+ Item + End Date + + Legacy Widget + Dec 31, 2026 + + + New Widget + Active + +
+
+
+ + +
+ Example 10: Value State with Links +

+ Value state messages can contain interactive links for help, documentation, or actions. + Focus on the input to see the message with links. +

+ +
+ +
+ Product not found in catalog. + View product guidelines + or + contact support. +
+ Product + Category + + PRD-001 + Electronics + +
+ + +
+ This vendor requires approval. + Request approval + or + choose alternative vendor. +
+ Vendor + Status + + Acme Corp + Pending + + + Beta Inc + Approved + +
+ + +
+ New search syntax available! + Learn about advanced search operators. +
+ Query + Results + + name:Widget + 15 matches + +
+
+
+ + +
+ Example 11: Disabled and Readonly +

+ Disabled inputs cannot be focused or edited. Readonly inputs can be focused but not edited. +

+ +
+ + Product + Price + + PRD-001 + $99 + + + PRD-002 + $149 + + + + + Product + Stock + + Widget X + 50 units + + + Widget Y + 25 units + + +
+
+ + +
+ Example 12: Many Suggestions (112 rows) +

+ This example demonstrates performance with a large number of suggestions (112 rows). + The popover should handle scrolling smoothly. +

+ + + SKU + Product Name + Category + Price + + +
Selected: (none)
+
+ + + + + + diff --git a/packages/website/docs/_components_pages/main/TabularInput/TabularInput.mdx b/packages/website/docs/_components_pages/main/TabularInput/TabularInput.mdx new file mode 100644 index 000000000000..f1e3f6b6725a --- /dev/null +++ b/packages/website/docs/_components_pages/main/TabularInput/TabularInput.mdx @@ -0,0 +1,26 @@ +--- +slug: ../../TabularInput +--- + +import Basic from "../../../_samples/main/TabularInput/Basic/Basic.md"; +import ScrollMode from "../../../_samples/main/TabularInput/ScrollMode/ScrollMode.md"; +import ValueState from "../../../_samples/main/TabularInput/ValueState/ValueState.md"; + +<%COMPONENT_OVERVIEW%> + +## Basic Sample + + +<%COMPONENT_METADATA%> + +## More Samples + +### Scroll Mode +When the number of suggestion columns exceeds the available width, you can enable horizontal scrolling using the `overflowMode="Scroll"` property. + + + +### Value State +The component supports different value states to indicate validation status. + + diff --git a/packages/website/docs/_samples/main/TabularInput/Basic/Basic.md b/packages/website/docs/_samples/main/TabularInput/Basic/Basic.md new file mode 100644 index 000000000000..0c062a836e84 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/Basic/Basic.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + diff --git a/packages/website/docs/_samples/main/TabularInput/Basic/main.js b/packages/website/docs/_samples/main/TabularInput/Basic/main.js new file mode 100644 index 000000000000..200b6d6b2e34 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/Basic/main.js @@ -0,0 +1,4 @@ +import "@ui5/webcomponents/dist/TabularInput.js"; +import "@ui5/webcomponents/dist/TableHeaderCell.js"; +import "@ui5/webcomponents/dist/TableRow.js"; +import "@ui5/webcomponents/dist/TableCell.js"; diff --git a/packages/website/docs/_samples/main/TabularInput/Basic/sample.html b/packages/website/docs/_samples/main/TabularInput/Basic/sample.html new file mode 100644 index 000000000000..0d9d90423abe --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/Basic/sample.html @@ -0,0 +1,45 @@ + + + + + + + + Sample + + + + + + + Product ID + Name + Category + Price + + + PRD-001 + Laptop Pro 15 + Electronics + $1,299 + + + PRD-002 + Smartphone X + Electronics + $899 + + + PRD-003 + Wireless Mouse + Accessories + $49 + + + + + + + + + diff --git a/packages/website/docs/_samples/main/TabularInput/Basic/sample.tsx b/packages/website/docs/_samples/main/TabularInput/Basic/sample.tsx new file mode 100644 index 000000000000..89fd8b96fe00 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/Basic/sample.tsx @@ -0,0 +1,42 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import TabularInputClass from "@ui5/webcomponents/dist/TabularInput.js"; +import TableHeaderCellClass from "@ui5/webcomponents/dist/TableHeaderCell.js"; +import TableRowClass from "@ui5/webcomponents/dist/TableRow.js"; +import TableCellClass from "@ui5/webcomponents/dist/TableCell.js"; + +const TabularInput = createReactComponent(TabularInputClass); +const TableHeaderCell = createReactComponent(TableHeaderCellClass); +const TableRow = createReactComponent(TableRowClass); +const TableCell = createReactComponent(TableCellClass); + +function App() { + return ( + + Product ID + Name + Category + Price + + + PRD-001 + Laptop Pro 15 + Electronics + $1,299 + + + PRD-002 + Smartphone X + Electronics + $899 + + + PRD-003 + Wireless Mouse + Accessories + $49 + + + ); +} + +export default App; diff --git a/packages/website/docs/_samples/main/TabularInput/ScrollMode/ScrollMode.md b/packages/website/docs/_samples/main/TabularInput/ScrollMode/ScrollMode.md new file mode 100644 index 000000000000..0c062a836e84 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/ScrollMode/ScrollMode.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + diff --git a/packages/website/docs/_samples/main/TabularInput/ScrollMode/main.js b/packages/website/docs/_samples/main/TabularInput/ScrollMode/main.js new file mode 100644 index 000000000000..200b6d6b2e34 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/ScrollMode/main.js @@ -0,0 +1,4 @@ +import "@ui5/webcomponents/dist/TabularInput.js"; +import "@ui5/webcomponents/dist/TableHeaderCell.js"; +import "@ui5/webcomponents/dist/TableRow.js"; +import "@ui5/webcomponents/dist/TableCell.js"; diff --git a/packages/website/docs/_samples/main/TabularInput/ScrollMode/sample.html b/packages/website/docs/_samples/main/TabularInput/ScrollMode/sample.html new file mode 100644 index 000000000000..fddd8ce2a679 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/ScrollMode/sample.html @@ -0,0 +1,45 @@ + + + + + + + + Sample + + + + + + + Name + Department + Location + Extension + + + John Smith + Engineering + Building A + x1234 + + + Jane Doe + Marketing + Building B + x2345 + + + James Wilson + Sales + Building A + x3456 + + + + + + + + + diff --git a/packages/website/docs/_samples/main/TabularInput/ScrollMode/sample.tsx b/packages/website/docs/_samples/main/TabularInput/ScrollMode/sample.tsx new file mode 100644 index 000000000000..0e6d090bfac0 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/ScrollMode/sample.tsx @@ -0,0 +1,42 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import TabularInputClass from "@ui5/webcomponents/dist/TabularInput.js"; +import TableHeaderCellClass from "@ui5/webcomponents/dist/TableHeaderCell.js"; +import TableRowClass from "@ui5/webcomponents/dist/TableRow.js"; +import TableCellClass from "@ui5/webcomponents/dist/TableCell.js"; + +const TabularInput = createReactComponent(TabularInputClass); +const TableHeaderCell = createReactComponent(TableHeaderCellClass); +const TableRow = createReactComponent(TableRowClass); +const TableCell = createReactComponent(TableCellClass); + +function App() { + return ( + + Name + Department + Location + Extension + + + John Smith + Engineering + Building A + x1234 + + + Jane Doe + Marketing + Building B + x2345 + + + James Wilson + Sales + Building A + x3456 + + + ); +} + +export default App; diff --git a/packages/website/docs/_samples/main/TabularInput/ValueState/ValueState.md b/packages/website/docs/_samples/main/TabularInput/ValueState/ValueState.md new file mode 100644 index 000000000000..0c062a836e84 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/ValueState/ValueState.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + diff --git a/packages/website/docs/_samples/main/TabularInput/ValueState/main.js b/packages/website/docs/_samples/main/TabularInput/ValueState/main.js new file mode 100644 index 000000000000..200b6d6b2e34 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/ValueState/main.js @@ -0,0 +1,4 @@ +import "@ui5/webcomponents/dist/TabularInput.js"; +import "@ui5/webcomponents/dist/TableHeaderCell.js"; +import "@ui5/webcomponents/dist/TableRow.js"; +import "@ui5/webcomponents/dist/TableCell.js"; diff --git a/packages/website/docs/_samples/main/TabularInput/ValueState/sample.html b/packages/website/docs/_samples/main/TabularInput/ValueState/sample.html new file mode 100644 index 000000000000..07256aa7e270 --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/ValueState/sample.html @@ -0,0 +1,51 @@ + + + + + + + + Sample + + + + + +
+ +
Product ID is invalid. Please enter a valid ID.
+ Product + Price + + PRD-001 + $99 + +
+ + +
Stock is running low. Consider reordering soon.
+ Product + Stock + + Widget X + 5 left + +
+ + +
Tip: You can use wildcards (*) for broader search.
+ Search + Results + + Widget* + 25 matches + +
+
+ + + + + + + diff --git a/packages/website/docs/_samples/main/TabularInput/ValueState/sample.tsx b/packages/website/docs/_samples/main/TabularInput/ValueState/sample.tsx new file mode 100644 index 000000000000..6034a8fee3ad --- /dev/null +++ b/packages/website/docs/_samples/main/TabularInput/ValueState/sample.tsx @@ -0,0 +1,48 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import TabularInputClass from "@ui5/webcomponents/dist/TabularInput.js"; +import TableHeaderCellClass from "@ui5/webcomponents/dist/TableHeaderCell.js"; +import TableRowClass from "@ui5/webcomponents/dist/TableRow.js"; +import TableCellClass from "@ui5/webcomponents/dist/TableCell.js"; + +const TabularInput = createReactComponent(TabularInputClass); +const TableHeaderCell = createReactComponent(TableHeaderCellClass); +const TableRow = createReactComponent(TableRowClass); +const TableCell = createReactComponent(TableCellClass); + +function App() { + return ( +
+ +
Product ID is invalid. Please enter a valid ID.
+ Product + Price + + PRD-001 + $99 + +
+ + +
Stock is running low. Consider reordering soon.
+ Product + Stock + + Widget X + 5 left + +
+ + +
Tip: You can use wildcards (*) for broader search.
+ Search + Results + + Widget* + 25 matches + +
+
+ ); +} + +export default App;