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 c41e811ac..a0cee2234 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 ee77ecfe4..09ab62807 100644
--- a/packages/raystack/components/data-view/__tests__/data-view.test.tsx
+++ b/packages/raystack/components/data-view/__tests__/data-view.test.tsx
@@ -390,6 +390,36 @@ describe('DataView', () => {
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('status region', () => {
beforeEach(() => {
vi.useFakeTimers();
diff --git a/packages/raystack/components/data-view/components/display-controls.tsx b/packages/raystack/components/data-view/components/display-controls.tsx
index ce77f3e82..d90a670c8 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,9 @@ export function DisplayControls({
hideViewSwitcher = false,
hideOrdering = false,
hideGrouping = false,
- hideDisplayProperties = false
+ hideDisplayProperties = false,
+ align = 'end',
+ ...positionerProps
}: DisplayControlsProps) {
const {
fields,
@@ -83,7 +87,8 @@ 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 deac06bf0..fa6a3002a 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)
@@ -76,7 +80,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;
@@ -105,7 +109,8 @@ export interface DataViewFiltersProps {
export function Filters({
classNames,
className,
- trigger
+ trigger,
+ ...positionerProps
}: DataViewFiltersProps) {
const { fields, tableQuery } = useDataView();
@@ -168,6 +173,7 @@ export function Filters({
appliedFiltersSet={appliedFiltersSet}
onAddFilter={onAddFilter}
className={classNames?.addFilter}
+ {...positionerProps}
>
{trigger}
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 (
{
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}