From ae17be64672f4f60b0edc9b3b06f7dc04972784d Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 29 Sep 2026 19:19:55 +0530 Subject: [PATCH 1/3] feat: [data-view] forward popup position props and Search leadingIcon --- .../content/docs/components/search/demo.ts | 9 ++++ .../content/docs/components/search/index.mdx | 8 +++- .../content/docs/components/search/props.ts | 6 +++ apps/www/src/content/docs/dataview/index.mdx | 10 ++++- apps/www/src/content/docs/dataview/props.ts | 42 +++++++++++++++++++ .../data-view/__tests__/data-view.test.tsx | 30 +++++++++++++ .../data-view/components/display-controls.tsx | 8 +++- .../data-view/components/filters.tsx | 16 ++++--- .../search/__tests__/search.test.tsx | 15 +++++++ .../raystack/components/search/search.tsx | 5 ++- 10 files changed, 138 insertions(+), 11 deletions(-) diff --git a/apps/www/src/content/docs/components/search/demo.ts b/apps/www/src/content/docs/components/search/demo.ts index cf828a5b1..5e3c07abc 100644 --- a/apps/www/src/content/docs/components/search/demo.ts +++ b/apps/www/src/content/docs/components/search/demo.ts @@ -31,6 +31,15 @@ export const sizeDemo = { ` }; +export const leadingIconDemo = { + type: 'code', + code: ` + + } /> + + ` +}; + export const clearDemo = { type: 'code', code: ` diff --git a/apps/www/src/content/docs/components/search/index.mdx b/apps/www/src/content/docs/components/search/index.mdx index 6536c9ab7..820898de8 100644 --- a/apps/www/src/content/docs/components/search/index.mdx +++ b/apps/www/src/content/docs/components/search/index.mdx @@ -4,7 +4,7 @@ description: A search input component with built-in search icon and optional cle source: packages/raystack/components/search --- -import { playground, sizeDemo, clearDemo, onValueChangeDemo } from "./demo.ts"; +import { playground, sizeDemo, leadingIconDemo, clearDemo, onValueChangeDemo } from "./demo.ts"; @@ -28,6 +28,12 @@ Two sizes. `large` is the default; `small` fits a toolbar or a table header. +### Leading icon + +Pass a node to `leadingIcon` to replace the search icon, or `null` to hide it. + + + ### Clear Button The Search component can include a clear button that appears when there is input value. diff --git a/apps/www/src/content/docs/components/search/props.ts b/apps/www/src/content/docs/components/search/props.ts index 8d04db533..cedc58465 100644 --- a/apps/www/src/content/docs/components/search/props.ts +++ b/apps/www/src/content/docs/components/search/props.ts @@ -11,6 +11,12 @@ export interface SearchProps { /** Whether the search input is disabled. */ disabled?: boolean; + /** + * Icon before the input. Pass `null` to hide it. + * @default + */ + leadingIcon?: React.ReactNode; + /** Shows a clear button when the input has a value. */ showClearButton?: boolean; diff --git a/apps/www/src/content/docs/dataview/index.mdx b/apps/www/src/content/docs/dataview/index.mdx index fbd8d0e25..8257433a1 100644 --- a/apps/www/src/content/docs/dataview/index.mdx +++ b/apps/www/src/content/docs/dataview/index.mdx @@ -87,6 +87,8 @@ The split is what lets one set of filters, sorts, and toggles drive every render By default search auto-disables in the zero state (no data and no active query) and re-enables the moment the user types. Pass `autoDisableInZeroState={false}` to keep it always enabled, or `disabled` to control it yourself. In server mode, read `query.search` in `onTableQueryChange` and filter on the backend. +`DataView.Search` accepts every [Search](/docs/components/search) prop. For example, `leadingIcon={null}` hides the search icon. + ### Display properties Column visibility is a **single global map** on context. `DataView.List` honours it for free, since TanStack column visibility hides the grid track. For free-form renderers, wrap fields in `DataView.DisplayAccess`: @@ -239,9 +241,15 @@ The shape `query` accepts and `onTableQueryChange` hands back. In [server mode]( +### DataView.Filters + +The row of applied filter chips and the add-filter menu. `align`, `side`, and `sideOffset` position the menu. + + + ### DataView.DisplayControls -The popover housing the view switcher, Ordering, Grouping, and Display Properties. The view switcher appears at the top whenever `views.length > 1`. Each section can be hidden individually. +The popover housing the view switcher, Ordering, Grouping, and Display Properties. The view switcher appears at the top whenever `views.length > 1`. Each section can be hidden individually. `align`, `side`, and `sideOffset` position the popover. diff --git a/apps/www/src/content/docs/dataview/props.ts b/apps/www/src/content/docs/dataview/props.ts index 91a8bd61f..ffbed5a8b 100644 --- a/apps/www/src/content/docs/dataview/props.ts +++ b/apps/www/src/content/docs/dataview/props.ts @@ -433,9 +433,51 @@ export interface DataViewClearFiltersProps { className?: string; } +export interface DataViewFiltersProps { + /** Custom trigger for the add-filter menu. A function receives the available and applied filters. */ + trigger?: + | ReactNode + | ((args: { + availableFilters: DataViewField[]; + appliedFilters: Set; + }) => ReactNode); + /** Class applied to the filters row. */ + className?: string; + /** + * Alignment of the add-filter menu against its trigger. + * @default "start" + */ + align?: 'start' | 'center' | 'end'; + /** + * Side of the trigger that the add-filter menu opens on. + * @default "bottom" + */ + side?: 'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start'; + /** + * Distance in pixels between the trigger and the add-filter menu. + * @default 4 + */ + sideOffset?: number; +} + export interface DataViewDisplayControlsProps { /** Custom trigger element for the popover. */ trigger?: ReactNode; + /** + * Alignment of the popover against its trigger. + * @default "end" + */ + align?: 'start' | 'center' | 'end'; + /** + * Side of the trigger that the popover opens on. + * @default "bottom" + */ + side?: 'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start'; + /** + * Distance in pixels between the trigger and the popover. + * @default 4 + */ + sideOffset?: number; /** Hide the multi-view switcher (shown by default when `views.length > 1`). */ hideViewSwitcher?: boolean; /** Hide the Ordering (sort) control. */ diff --git a/packages/raystack/components/data-view/__tests__/data-view.test.tsx b/packages/raystack/components/data-view/__tests__/data-view.test.tsx index 9e98859d4..de42d45d7 100644 --- a/packages/raystack/components/data-view/__tests__/data-view.test.tsx +++ b/packages/raystack/components/data-view/__tests__/data-view.test.tsx @@ -381,6 +381,36 @@ describe('DataView', () => { expect(screen.queryByText('John Doe')).not.toBeInTheDocument(); expect(screen.getByText('Jane Smith')).toBeInTheDocument(); }); + + it('Filters forwards positioning props to the add-filter menu', async () => { + const user = userEvent.setup(); + render( + + + + + ); + await user.click(screen.getByRole('button', { name: /filter/i })); + const item = await screen.findByRole('menuitem', { name: 'Name' }); + const positioner = item.closest('[data-align]'); + expect(positioner).toHaveAttribute('data-align', 'end'); + expect(positioner).toHaveAttribute('data-side', 'top'); + }); + + it('DisplayControls forwards positioning props to the popover', async () => { + const user = userEvent.setup(); + render( + + + + + ); + await user.click(screen.getByRole('button', { name: 'Display' })); + const reset = await screen.findByText('Reset to default'); + const positioner = reset.closest('[data-align]'); + expect(positioner).toHaveAttribute('data-align', 'start'); + expect(positioner).toHaveAttribute('data-side', 'top'); + }); }); describe('Multi-view', () => { diff --git a/packages/raystack/components/data-view/components/display-controls.tsx b/packages/raystack/components/data-view/components/display-controls.tsx index ce77f3e82..d9336e958 100644 --- a/packages/raystack/components/data-view/components/display-controls.tsx +++ b/packages/raystack/components/data-view/components/display-controls.tsx @@ -6,6 +6,7 @@ import { DisplayIcon } from '~/icons'; import { Button } from '../../button'; import { Flex } from '../../flex'; import { Popover } from '../../popover'; +import type { PopoverContentProps } from '../../popover/popover'; import styles from '../data-view.module.css'; import { defaultGroupOption, SortOrdersValues } from '../data-view.types'; import { useDataView } from '../hooks/useDataView'; @@ -14,7 +15,8 @@ import { Grouping } from './grouping'; import { Ordering } from './ordering'; import { ViewSwitcher } from './view-switcher'; -interface DisplayControlsProps { +interface DisplayControlsProps + extends Pick { trigger?: ReactNode; hideViewSwitcher?: boolean; hideOrdering?: boolean; @@ -44,7 +46,8 @@ export function DisplayControls({ hideViewSwitcher = false, hideOrdering = false, hideGrouping = false, - hideDisplayProperties = false + hideDisplayProperties = false, + ...positionerProps }: DisplayControlsProps) { const { fields, @@ -84,6 +87,7 @@ export function DisplayControls({ {showViewSwitcher ? ( diff --git a/packages/raystack/components/data-view/components/filters.tsx b/packages/raystack/components/data-view/components/filters.tsx index c2e55708f..57c2f3bdb 100644 --- a/packages/raystack/components/data-view/components/filters.tsx +++ b/packages/raystack/components/data-view/components/filters.tsx @@ -9,6 +9,7 @@ import { FilterChip } from '../../filter-chip'; import { Flex } from '../../flex'; import { IconButton } from '../../icon-button'; import { Menu } from '../../menu'; +import type { MenuContentProps } from '../../menu/menu-content'; import styles from '../data-view.module.css'; import { DataViewField } from '../data-view.types'; import { useDataView } from '../hooks/useDataView'; @@ -21,7 +22,9 @@ type Trigger = appliedFilters: Set; }) => ReactNode); -interface AddFilterProps { +type PositionerProps = Pick; + +interface AddFilterProps extends PositionerProps { fieldList: DataViewField[]; appliedFiltersSet: Set; onAddFilter: (field: DataViewField) => void; @@ -35,7 +38,8 @@ function AddFilter({ appliedFiltersSet, onAddFilter, children, - className + className, + ...positionerProps }: AddFilterProps) { const availableFilters = fieldList?.filter( f => !appliedFiltersSet.has(f.accessorKey) @@ -75,7 +79,7 @@ function AddFilter({ {trigger}} /> - + {availableFilters?.map(field => ( ({ ) : null; } -export interface DataViewFiltersProps { +export interface DataViewFiltersProps extends PositionerProps { classNames?: { /** @deprecated Use `[data-slot="filter-chip"]` instead. */ filterChips?: string; @@ -104,7 +108,8 @@ export interface DataViewFiltersProps { export function Filters({ classNames, className, - trigger + trigger, + ...positionerProps }: DataViewFiltersProps) { const { fields, tableQuery } = useDataView(); @@ -167,6 +172,7 @@ export function Filters({ appliedFiltersSet={appliedFiltersSet} onAddFilter={onAddFilter} className={classNames?.addFilter} + {...positionerProps} > {trigger} diff --git a/packages/raystack/components/search/__tests__/search.test.tsx b/packages/raystack/components/search/__tests__/search.test.tsx index 6321f2fdd..14ff2c446 100644 --- a/packages/raystack/components/search/__tests__/search.test.tsx +++ b/packages/raystack/components/search/__tests__/search.test.tsx @@ -30,6 +30,21 @@ describe('Search', () => { expect(svg).toBeInTheDocument(); }); + it('replaces the search icon with a custom leadingIcon', () => { + const { container } = render( + } /> + ); + expect(screen.getByTestId('custom-icon')).toBeInTheDocument(); + expect(container.querySelector('svg')).toBeNull(); + }); + + it('hides the leading icon when leadingIcon is null', () => { + const { container } = render(); + expect( + container.querySelector('[data-slot="input-leading-icon"]') + ).toBeNull(); + }); + it('applies default placeholder', () => { render(); const input = screen.getByPlaceholderText('Search'); diff --git a/packages/raystack/components/search/search.tsx b/packages/raystack/components/search/search.tsx index ced3a5ab5..5209cf409 100644 --- a/packages/raystack/components/search/search.tsx +++ b/packages/raystack/components/search/search.tsx @@ -7,7 +7,7 @@ import { InputProps } from '../input/input'; import styles from './search.module.css'; -export interface SearchProps extends Omit { +export interface SearchProps extends InputProps { showClearButton?: boolean; onClear?: () => void; variant?: 'default' | 'borderless'; @@ -22,6 +22,7 @@ export function Search({ value, width = '100%', variant = 'default', + leadingIcon = , ...props }: SearchProps) { const trailingIconWithClear = showClearButton ? ( @@ -53,7 +54,7 @@ export function Search({ > } + leadingIcon={leadingIcon} trailingIcon={trailingIconWithClear} placeholder={placeholder} disabled={disabled} From 7ee7e64e587d6e84b26d4313cc5f41ab94b4f6c6 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Mon, 5 Oct 2026 01:11:33 +0530 Subject: [PATCH 2/3] fix: [data-view] keep popup position defaults when props are undefined --- .../data-view/__tests__/data-view.test.tsx | 16 ++++++++++++++++ .../data-view/components/display-controls.tsx | 3 ++- packages/raystack/components/popover/popover.tsx | 3 ++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/raystack/components/data-view/__tests__/data-view.test.tsx b/packages/raystack/components/data-view/__tests__/data-view.test.tsx index de42d45d7..e1944b43c 100644 --- a/packages/raystack/components/data-view/__tests__/data-view.test.tsx +++ b/packages/raystack/components/data-view/__tests__/data-view.test.tsx @@ -411,6 +411,22 @@ describe('DataView', () => { expect(positioner).toHaveAttribute('data-align', 'start'); expect(positioner).toHaveAttribute('data-side', 'top'); }); + + it('DisplayControls keeps align end when align is undefined', async () => { + const user = userEvent.setup(); + render( + + + + + ); + await user.click(screen.getByRole('button', { name: 'Display' })); + const reset = await screen.findByText('Reset to default'); + expect(reset.closest('[data-align]')).toHaveAttribute( + 'data-align', + 'end' + ); + }); }); describe('Multi-view', () => { diff --git a/packages/raystack/components/data-view/components/display-controls.tsx b/packages/raystack/components/data-view/components/display-controls.tsx index d9336e958..d90a670c8 100644 --- a/packages/raystack/components/data-view/components/display-controls.tsx +++ b/packages/raystack/components/data-view/components/display-controls.tsx @@ -47,6 +47,7 @@ export function DisplayControls({ hideOrdering = false, hideGrouping = false, hideDisplayProperties = false, + align = 'end', ...positionerProps }: DisplayControlsProps) { const { @@ -86,7 +87,7 @@ export function DisplayControls({ /> diff --git a/packages/raystack/components/popover/popover.tsx b/packages/raystack/components/popover/popover.tsx index dd9b48be1..92e85d3e4 100644 --- a/packages/raystack/components/popover/popover.tsx +++ b/packages/raystack/components/popover/popover.tsx @@ -25,13 +25,14 @@ function PopoverContent({ render, children, radius, + sideOffset = 4, ...positionerProps }: PopoverContentProps) { const theme = useThemeInjection(); return ( Date: Mon, 5 Oct 2026 01:14:05 +0530 Subject: [PATCH 3/3] test: [data-view] remove align undefined test --- .../data-view/__tests__/data-view.test.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/raystack/components/data-view/__tests__/data-view.test.tsx b/packages/raystack/components/data-view/__tests__/data-view.test.tsx index e1944b43c..de42d45d7 100644 --- a/packages/raystack/components/data-view/__tests__/data-view.test.tsx +++ b/packages/raystack/components/data-view/__tests__/data-view.test.tsx @@ -411,22 +411,6 @@ describe('DataView', () => { expect(positioner).toHaveAttribute('data-align', 'start'); expect(positioner).toHaveAttribute('data-side', 'top'); }); - - it('DisplayControls keeps align end when align is undefined', async () => { - const user = userEvent.setup(); - render( - - - - - ); - await user.click(screen.getByRole('button', { name: 'Display' })); - const reset = await screen.findByText('Reset to default'); - expect(reset.closest('[data-align]')).toHaveAttribute( - 'data-align', - 'end' - ); - }); }); describe('Multi-view', () => {