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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/www/src/content/docs/components/search/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ export const sizeDemo = {
</Flex>`
};

export const leadingIconDemo = {
type: 'code',
code: `
<Flex direction="column" gap={5} align="center">
<Search placeholder="Filter..." leadingIcon={<FilterIcon />} />
<Search placeholder="No icon..." leadingIcon={null} />
</Flex>`
};

export const clearDemo = {
type: 'code',
code: `
Expand Down
8 changes: 7 additions & 1 deletion apps/www/src/content/docs/components/search/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

<Demo data={playground} />

Expand All @@ -28,6 +28,12 @@ Two sizes. `large` is the default; `small` fits a toolbar or a table header.

<Demo data={sizeDemo} />

### Leading icon

Pass a node to `leadingIcon` to replace the search icon, or `null` to hide it.
Comment thread
rohanchkrabrty marked this conversation as resolved.

<Demo data={leadingIconDemo} />

### Clear Button

The Search component can include a clear button that appears when there is input value.
Expand Down
6 changes: 6 additions & 0 deletions apps/www/src/content/docs/components/search/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export interface SearchProps {
/** Whether the search input is disabled. */
disabled?: boolean;

/**
* Icon before the input. Pass `null` to hide it.
Comment thread
rohanchkrabrty marked this conversation as resolved.
* @default <SearchIcon />
*/
leadingIcon?: React.ReactNode;

/** Shows a clear button when the input has a value. */
showClearButton?: boolean;

Expand Down
10 changes: 9 additions & 1 deletion apps/www/src/content/docs/dataview/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -239,9 +241,15 @@ The shape `query` accepts and `onTableQueryChange` hands back. In [server mode](

<auto-type-table path="./props.ts" name="DataViewZeroStateProps" />

### DataView.Filters

The row of applied filter chips and the add-filter menu. `align`, `side`, and `sideOffset` position the menu.

<auto-type-table path="./props.ts" name="DataViewFiltersProps" />

### 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.

<auto-type-table path="./props.ts" name="DataViewDisplayControlsProps" />

Expand Down
42 changes: 42 additions & 0 deletions apps/www/src/content/docs/dataview/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
}) => 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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<DataView data={mockData} fields={mockFields} defaultSort={defaultSort}>
<DataView.Filters align='end' side='top' />
<DataView.List variant='table' columns={mockColumns} />
</DataView>
);
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(
<DataView data={mockData} fields={mockFields} defaultSort={defaultSort}>
<DataView.DisplayControls align='start' side='top' />
<DataView.List variant='table' columns={mockColumns} />
</DataView>
);
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -14,7 +15,8 @@ import { Grouping } from './grouping';
import { Ordering } from './ordering';
import { ViewSwitcher } from './view-switcher';

interface DisplayControlsProps {
interface DisplayControlsProps
extends Pick<PopoverContentProps, 'align' | 'side' | 'sideOffset'> {
trigger?: ReactNode;
hideViewSwitcher?: boolean;
hideOrdering?: boolean;
Expand Down Expand Up @@ -44,7 +46,9 @@ export function DisplayControls<TData>({
hideViewSwitcher = false,
hideOrdering = false,
hideGrouping = false,
hideDisplayProperties = false
hideDisplayProperties = false,
align = 'end',
...positionerProps
}: DisplayControlsProps) {
const {
fields,
Expand Down Expand Up @@ -83,7 +87,8 @@ export function DisplayControls<TData>({
/>
<Popover.Content
className={styles['display-popover-content']}
align='end'
align={align}
{...positionerProps}
>
<Flex direction='column' data-slot='data-view-display-content'>
{showViewSwitcher ? (
Expand Down
16 changes: 11 additions & 5 deletions packages/raystack/components/data-view/components/filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -21,7 +22,9 @@ type Trigger<TData> =
appliedFilters: Set<string>;
}) => ReactNode);

interface AddFilterProps<TData> {
type PositionerProps = Pick<MenuContentProps, 'align' | 'side' | 'sideOffset'>;

interface AddFilterProps<TData> extends PositionerProps {
fieldList: DataViewField<TData>[];
appliedFiltersSet: Set<string>;
onAddFilter: (field: DataViewField<TData>) => void;
Expand All @@ -35,7 +38,8 @@ function AddFilter<TData>({
appliedFiltersSet,
onAddFilter,
children,
className
className,
...positionerProps
}: AddFilterProps<TData>) {
const availableFilters = fieldList?.filter(
f => !appliedFiltersSet.has(f.accessorKey)
Expand Down Expand Up @@ -76,7 +80,7 @@ function AddFilter<TData>({
<Menu.Trigger
render={isValidElement(trigger) ? trigger : <button>{trigger}</button>}
/>
<Menu.Content>
<Menu.Content {...positionerProps}>
{availableFilters?.map(field => (
<Menu.Item
key={field.accessorKey}
Expand All @@ -91,7 +95,7 @@ function AddFilter<TData>({
) : null;
}

export interface DataViewFiltersProps<TData> {
export interface DataViewFiltersProps<TData> extends PositionerProps {
classNames?: {
/** @deprecated Use `[data-slot="filter-chip"]` instead. */
filterChips?: string;
Expand All @@ -105,7 +109,8 @@ export interface DataViewFiltersProps<TData> {
export function Filters<TData>({
classNames,
className,
trigger
trigger,
...positionerProps
}: DataViewFiltersProps<TData>) {
const { fields, tableQuery } = useDataView<TData>();

Expand Down Expand Up @@ -168,6 +173,7 @@ export function Filters<TData>({
appliedFiltersSet={appliedFiltersSet}
onAddFilter={onAddFilter}
className={classNames?.addFilter}
{...positionerProps}
>
{trigger}
</AddFilter>
Expand Down
3 changes: 2 additions & 1 deletion packages/raystack/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ function PopoverContent({
render,
children,
radius,
sideOffset = 4,
...positionerProps
}: PopoverContentProps) {
const theme = useThemeInjection();
return (
<PopoverPrimitive.Portal {...theme}>
<PopoverPrimitive.Positioner
sideOffset={4}
sideOffset={sideOffset}
collisionPadding={3}
className={styles.popoverPositioner}
data-slot='popover-positioner'
Expand Down
15 changes: 15 additions & 0 deletions packages/raystack/components/search/__tests__/search.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ describe('Search', () => {
expect(svg).toBeInTheDocument();
});

it('replaces the search icon with a custom leadingIcon', () => {
const { container } = render(
<Search leadingIcon={<span data-testid='custom-icon' />} />
);
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
expect(container.querySelector('svg')).toBeNull();
});

it('hides the leading icon when leadingIcon is null', () => {
const { container } = render(<Search leadingIcon={null} />);
expect(
container.querySelector('[data-slot="input-leading-icon"]')
).toBeNull();
});

it('applies default placeholder', () => {
render(<Search />);
const input = screen.getByPlaceholderText('Search');
Expand Down
5 changes: 3 additions & 2 deletions packages/raystack/components/search/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { InputProps } from '../input/input';

import styles from './search.module.css';

export interface SearchProps extends Omit<InputProps, 'leadingIcon'> {
export interface SearchProps extends InputProps {
showClearButton?: boolean;
onClear?: () => void;
variant?: 'default' | 'borderless';
Expand All @@ -22,6 +22,7 @@ export function Search({
value,
width = '100%',
variant = 'default',
leadingIcon = <SearchIcon />,
...props
}: SearchProps) {
const trailingIconWithClear = showClearButton ? (
Expand Down Expand Up @@ -53,7 +54,7 @@ export function Search({
>
<Input
data-slot='search-input'
leadingIcon={<SearchIcon />}
leadingIcon={leadingIcon}
trailingIcon={trailingIconWithClear}
placeholder={placeholder}
disabled={disabled}
Expand Down
Loading