npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@abaktiar/ql-input

v1.5.0

Published

React QL input component with intelligent autocomplete, parameterized functions, and query validation. Framework-independent with zero external dependencies.

Downloads

22

Readme

@abaktiar/ql-input

A comprehensive React QL (Query Language) input component with intelligent autocomplete, query validation, and a beautiful UI built with TypeScript and shadcn/ui components.

🌐 Live Demo - Try the component in action!

npm version TypeScript React License: MIT

🎉 What's New in v1.5.0

Smart Suggestion Behavior - Enhanced UX with manual control and Ctrl+Space shortcuts
🚀 Smooth & Minimal Design - Lightning-fast 80ms transitions with beautiful visual states
⌨️ Enhanced Keyboard Navigation - Auto-scroll to selection with seamless long-list navigation
🎯 Perfect Accessibility - Unified mouse and keyboard interactions with smooth scrolling
🔧 Parameterized Functions - Advanced function support with type validation and error handling

✨ Features

Core Functionality

  • 🔍 Context-aware autocomplete - Smart suggestions based on query context
  • 🎨 Visual feedback - Query validation status and condition count
  • ⌨️ Keyboard navigation - Full keyboard support (↑↓ Enter Esc Tab)
  • 🔧 Configurable fields - Define custom fields, operators, and values
  • 📝 Query parsing - Complete QL query structure parsing
  • 🎯 Parentheses grouping - Support for complex condition grouping

Advanced Features

  • 🌐 Async value suggestions - Server-side value fetching with debouncing
  • 🔄 Function support - Built-in functions like currentUser(), now(), and parameterized functions like daysAgo(30), userInRole("admin")
  • 📊 ORDER BY clauses - Sorting with ASC/DESC support
  • 🌙 Dark mode - Full theming support
  • Accessibility - ARIA attributes for screen readers
  • 📱 Responsive design - Works across different screen sizes

📦 Installation

npm install @abaktiar/ql-input
yarn add @abaktiar/ql-input
pnpm add @abaktiar/ql-input

Peer Dependencies

Make sure you have the required peer dependencies installed:

npm install react react-dom

🚀 Quick Start

Basic Setup

import React from 'react';
import { QLInput } from '@abaktiar/ql-input';
import '@abaktiar/ql-input/styles.css';
import type { QLInputConfig, QLQuery } from '@abaktiar/ql-input';

const config: QLInputConfig = {
  fields: [
    {
      name: 'status',
      displayName: 'Status',
      type: 'option',
      operators: ['=', '!=', 'IN', 'NOT IN'],
      options: [
        { value: 'open', label: 'Open' },
        { value: 'closed', label: 'Closed' },
        { value: 'pending', label: 'Pending' }
      ]
    },
    {
      name: 'assignee',
      displayName: 'Assignee',
      type: 'user',
      operators: ['=', '!=', 'IS EMPTY', 'IS NOT EMPTY']
    },
    {
      name: 'created',
      displayName: 'Created Date',
      type: 'date',
      operators: ['=', '>', '<', '>=', '<='],
      sortable: true
    }
  ],
  maxSuggestions: 10,
  caseSensitive: false,
  allowParentheses: true,
  allowOrderBy: true,
  allowFunctions: true
};

function MyComponent() {
  const handleChange = (value: string, query: QLQuery) => {
    console.log('Query changed:', value);
    console.log('Parsed query:', query);
  };

  const handleExecute = (query: QLQuery) => {
    console.log('Query executed:', query);
    // Handle query execution (e.g., API call)
  };

  return (
    <QLInput
      config={config}
      placeholder="Enter your query..."
      onChange={handleChange}
      onExecute={handleExecute}
    />
  );
}

Framework Independent

The component is completely framework-independent and doesn't require Tailwind CSS or any other CSS framework. Simply import the CSS file and you're ready to go:

@import '@abaktiar/ql-input/styles.css';

The component includes comprehensive styling with CSS custom properties for easy theming and dark mode support.

🎯 Component Props

QLInput Props

interface QLInputProps {
  // Configuration
  config: QLInputConfig;

  // Value control
  value?: string;
  onChange?: (value: string, query: QLQuery) => void;
  onExecute?: (query: QLQuery) => void;

  // Appearance
  placeholder?: string;
  disabled?: boolean;
  className?: string;
  showSearchIcon?: boolean;  // Default: true
  showClearIcon?: boolean;   // Default: true

  // Async suggestions
  getAsyncValueSuggestions?: (field: string, typedValue: string) => Promise<QLValue[]>;
  getPredefinedValueSuggestions?: (field: string) => QLValue[];
}

Configuration Options

interface QLInputConfig {
  fields: QLField[];
  maxSuggestions?: number;
  caseSensitive?: boolean;
  allowParentheses?: boolean;
  allowOrderBy?: boolean;
  allowFunctions?: boolean;
  functions?: QLFunction[];
}

💡 Usage Examples

Controlled Component

import React, { useState } from 'react';
import { QLInput } from '@abaktiar/ql-input';

function ControlledExample() {
  const [query, setQuery] = useState('status = "open"');

  return (
    <QLInput
      config={config}
      value={query}
      onChange={(value) => setQuery(value)}
      placeholder="Search issues..."
    />
  );
}

With Async Value Suggestions

import React from 'react';
import { QLInput } from '@abaktiar/ql-input';

function AsyncExample() {
  const getAsyncValueSuggestions = async (field: string, typedValue: string) => {
    if (field === 'assignee') {
      // Fetch users from API
      const response = await fetch(`/api/users?search=${typedValue}`);
      const users = await response.json();
      return users.map(user => ({
        value: user.id,
        label: user.name
      }));
    }
    return [];
  };

  return (
    <QLInput
      config={config}
      getAsyncValueSuggestions={getAsyncValueSuggestions}
      placeholder="Search with async suggestions..."
    />
  );
}

With Custom Functions

import React from 'react';
import { QLInput } from '@abaktiar/ql-input';

const configWithFunctions: QLInputConfig = {
  fields: [
    // ... your fields
  ],
  allowFunctions: true,
  functions: [
    {
      name: 'currentUser',
      displayName: 'currentUser()',
      description: 'The currently logged-in user',
    },
    {
      name: 'daysAgo',
      displayName: 'daysAgo(days)',
      description: 'Date N days ago from today',
      parameters: [{
        name: 'days',
        type: 'number',
        required: true,
        description: 'Number of days'
      }]
    },
    {
      name: 'userInRole',
      displayName: 'userInRole(role)',
      description: 'Users with specific role',
      parameters: [{
        name: 'role',
        type: 'text',
        required: true,
        description: 'User role name'
      }]
    },
    {
      name: 'dateRange',
      displayName: 'dateRange(start, end)',
      description: 'Date range between two dates',
      parameters: [
        {
          name: 'startDate',
          type: 'date',
          required: true,
          description: 'Start date'
        },
        {
          name: 'endDate',
          type: 'date',
          required: true,
          description: 'End date'
        }
      ]
    }
  ]
};

function CustomFunctionsExample() {
  return (
    <QLInput
      config={configWithFunctions}
      placeholder="Try: assignee = userInRole(&quot;admin&quot;) AND created >= daysAgo(30)"
    />
  );
}

🎯 Query Examples

The component supports powerful QL queries with parameterized functions:

Basic Function Queries

-- User functions
assignee = currentUser()
assignee = userInRole("admin")
assignee IN (currentUser(), userInRole("manager"))

-- Date functions
created >= daysAgo(30)
updated <= daysFromNow(7)
created = dateRange("2023-01-01", "2023-12-31")

-- Project functions
project = projectsWithPrefix("PROJ")

Complex Expressions

-- Combined conditions with functions
assignee = currentUser() AND created >= daysAgo(30)

-- Functions in IN lists
assignee IN (currentUser(), userInRole("admin")) AND priority = High

-- Complex grouping with functions
(created >= daysAgo(30) AND assignee = currentUser()) OR priority = Highest

-- Multiple function parameters
created = dateRange("2023-01-01", daysAgo(90)) AND status = Open

Real-World Use Cases

-- Find my recent work
assignee = currentUser() AND updated >= daysAgo(7)

-- Admin oversight
assignee = userInRole("admin") AND status = "In Progress"

-- Project timeline queries
project = projectsWithPrefix("PROJ") AND created >= dateRange("2023-01-01", "2023-12-31")

-- Team management
assignee IN (userInRole("developer"), userInRole("qa")) AND priority >= High

Form Integration

import React from 'react';
import { useForm } from 'react-hook-form';
import { QLInput } from '@abaktiar/ql-input';

interface FormData {
  query: string;
  // ... other fields
}

function FormExample() {
  const { register, handleSubmit, setValue, watch } = useForm<FormData>();
  const queryValue = watch('query');

  const onSubmit = (data: FormData) => {
    console.log('Form submitted:', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <QLInput
        config={config}
        value={queryValue}
        onChange={(value) => setValue('query', value)}
        placeholder="Enter search query..."
      />
      <button type="submit">Search</button>
    </form>
  );
}

Dark Mode Support

import React from 'react';
import { QLInput } from '@abaktiar/ql-input';

function DarkModeExample() {
  return (
    <div className="dark"> {/* or use ql-dark, or data-theme="dark" */}
      <QLInput
        config={config}
        placeholder="Dark mode supported automatically..."
      />
    </div>
  );
}

The component automatically detects dark mode using these selectors:

  • .dark (Tailwind CSS convention)
  • .ql-dark (component-specific)
  • [data-theme="dark"] (data attribute approach)

🎨 Styling & Customization

CSS Custom Properties

The component uses CSS custom properties for comprehensive theming:

:root {
  /* Colors */
  --ql-input-border: #e2e8f0;
  --ql-input-background: #ffffff;
  --ql-input-foreground: #1a202c;
  --ql-input-muted: #f7fafc;
  --ql-input-muted-foreground: #718096;
  --ql-input-ring: #3182ce;
  --ql-input-destructive: #e53e3e;
  --ql-input-success: #38a169;
  --ql-input-warning: #d69e2e;

  /* Spacing and sizing */
  --ql-input-radius: 0.375rem;
  --ql-input-spacing-sm: 0.5rem;
  --ql-input-spacing-md: 0.75rem;

  /* Typography */
  --ql-input-font-size-sm: 0.875rem;
  --ql-input-line-height: 1.5;

  /* Transitions */
  --ql-input-transition: all 0.2s ease-in-out;
}

/* Dark theme */
.dark .ql-input-container,
.ql-dark,
[data-theme="dark"] .ql-input-container {
  --ql-input-border: #4a5568;
  --ql-input-background: #2d3748;
  --ql-input-foreground: #f7fafc;
  --ql-input-muted: #4a5568;
  --ql-input-muted-foreground: #a0aec0;
  --ql-input-ring: #63b3ed;
  --ql-input-destructive: #fc8181;
  --ql-input-success: #68d391;
  --ql-input-warning: #f6e05e;
}

Custom Styling

You can customize the component by overriding CSS custom properties:

/* Custom theme */
.my-custom-theme {
  --ql-input-border: #3b82f6;
  --ql-input-ring: #1d4ed8;
  --ql-input-background: #f8fafc;
  --ql-input-radius: 0.75rem;
}
import React from 'react';
import { QLInput } from '@abaktiar/ql-input';

function StyledExample() {
  return (
    <div className="my-custom-theme">
      <QLInput
        config={config}
        placeholder="Custom styled input..."
      />
    </div>
  );
}

🔧 Advanced Usage

Using the Hook Directly

import React from 'react';
import { useQLInput } from '@abaktiar/ql-input';

function CustomInputComponent() {
  const { state, handleInputChange, handleKeyDown, selectSuggestion } = useQLInput({
    config,
    onChange: (value, query) => console.log(value, query)
  });

  return (
    <div>
      <input
        value={state.value}
        onChange={handleInputChange}
        onKeyDown={handleKeyDown}
      />
      {state.showSuggestions && (
        <div>
          {state.suggestions.map((suggestion, index) => (
            <div
              key={index}
              onClick={() => selectSuggestion(suggestion)}
              className={index === state.selectedSuggestionIndex ? 'selected' : ''}
            >
              {suggestion.label}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Custom Suggestion Rendering

import React from 'react';
import { QLInput, QLSuggestion } from '@abaktiar/ql-input';

function CustomSuggestionExample() {
  const renderSuggestion = (suggestion: QLSuggestion) => (
    <div className="flex items-center gap-2">
      <span className="font-medium">{suggestion.label}</span>
      {suggestion.description && (
        <span className="text-sm text-gray-500">{suggestion.description}</span>
      )}
    </div>
  );

  // Note: This is a conceptual example - actual implementation may vary
  return (
    <QLInput
      config={config}
      placeholder="Custom suggestions..."
    />
  );
}

🧪 Testing

Testing with React Testing Library

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { QLInput } from '@abaktiar/ql-input';

test('renders QL input component', () => {
  render(<QLInput config={config} placeholder="Test input" />);

  const input = screen.getByPlaceholderText('Test input');
  expect(input).toBeInTheDocument();
});

test('handles input changes', () => {
  const handleChange = jest.fn();

  render(
    <QLInput
      config={config}
      onChange={handleChange}
      placeholder="Test input"
    />
  );

  const input = screen.getByPlaceholderText('Test input');
  fireEvent.change(input, { target: { value: 'status = "open"' } });

  expect(handleChange).toHaveBeenCalled();
});

🤝 Related Packages

📄 License

MIT © abaktiar

🎯 Field Types & Operators

Supported Field Types

type FieldType = 'text' | 'number' | 'date' | 'datetime' | 'boolean' | 'option' | 'multiselect' | 'user';

Text Fields

{
  name: 'title',
  displayName: 'Title',
  type: 'text',
  operators: ['=', '!=', '~', '!~', 'IS EMPTY', 'IS NOT EMPTY']
}

Number Fields

{
  name: 'priority',
  displayName: 'Priority',
  type: 'number',
  operators: ['=', '!=', '>', '<', '>=', '<=']
}

Date/DateTime Fields

{
  name: 'created',
  displayName: 'Created Date',
  type: 'date', // or 'datetime'
  operators: ['=', '!=', '>', '<', '>=', '<='],
  sortable: true
}

Option Fields

{
  name: 'status',
  displayName: 'Status',
  type: 'option',
  operators: ['=', '!=', 'IN', 'NOT IN'],
  options: [
    { value: 'open', label: 'Open' },
    { value: 'closed', label: 'Closed' }
  ]
}

Boolean Fields

{
  name: 'isPublic',
  displayName: 'Is Public',
  type: 'boolean',
  operators: ['=', '!=']
}

User Fields

{
  name: 'assignee',
  displayName: 'Assignee',
  type: 'user',
  operators: ['=', '!=', 'IS EMPTY', 'IS NOT EMPTY'],
  asyncValueSuggestions: true
}

Complete Configuration Example

import { QLInputConfig } from '@abaktiar/ql-input';

const fullConfig: QLInputConfig = {
  fields: [
    {
      name: 'title',
      displayName: 'Title',
      type: 'text',
      operators: ['=', '!=', '~', '!~', 'IS EMPTY', 'IS NOT EMPTY'],
      description: 'Issue title or summary'
    },
    {
      name: 'status',
      displayName: 'Status',
      type: 'option',
      operators: ['=', '!=', 'IN', 'NOT IN'],
      options: [
        { value: 'open', label: 'Open' },
        { value: 'in-progress', label: 'In Progress' },
        { value: 'closed', label: 'Closed' },
        { value: 'pending', label: 'Pending Review' }
      ]
    },
    {
      name: 'priority',
      displayName: 'Priority',
      type: 'number',
      operators: ['=', '!=', '>', '<', '>=', '<='],
      sortable: true,
      options: [
        { value: '1', label: 'Low' },
        { value: '2', label: 'Medium' },
        { value: '3', label: 'High' },
        { value: '4', label: 'Critical' }
      ]
    },
    {
      name: 'assignee',
      displayName: 'Assignee',
      type: 'user',
      operators: ['=', '!=', 'IS EMPTY', 'IS NOT EMPTY'],
      asyncValueSuggestions: true
    },
    {
      name: 'created',
      displayName: 'Created Date',
      type: 'date',
      operators: ['=', '!=', '>', '<', '>=', '<='],
      sortable: true
    },
    {
      name: 'tags',
      displayName: 'Tags',
      type: 'multiselect',
      operators: ['IN', 'NOT IN'],
      options: [
        { value: 'bug', label: 'Bug' },
        { value: 'feature', label: 'Feature' },
        { value: 'enhancement', label: 'Enhancement' }
      ]
    }
  ],
  maxSuggestions: 15,
  caseSensitive: false,
  allowParentheses: true,
  allowOrderBy: true,
  allowFunctions: true,
  functions: [
    {
      name: 'currentUser',
      displayName: 'currentUser()',
      description: 'The currently logged-in user',
    },
    {
      name: 'now',
      displayName: 'now()',
      description: 'Current date and time',
    },
    {
      name: 'today',
      displayName: 'today()',
      description: 'Today\'s date',
    },
    {
      name: 'daysAgo',
      displayName: 'daysAgo(days)',
      description: 'Date N days ago from today',
      parameters: [{
        name: 'days',
        type: 'number',
        required: true,
        description: 'Number of days'
      }]
    },
    {
      name: 'userInRole',
      displayName: 'userInRole(role)',
      description: 'Users with specific role',
      parameters: [{
        name: 'role',
        type: 'text',
        required: true,
        description: 'User role name'
      }]
    }
  ]
};

🚀 Real-World Examples

Issue Tracking System

import React, { useState } from 'react';
import { QLInput, QLQuery } from '@abaktiar/ql-input';

function IssueTracker() {
  const [issues, setIssues] = useState([]);
  const [loading, setLoading] = useState(false);

  const handleSearch = async (query: QLQuery) => {
    if (!query.valid) return;

    setLoading(true);
    try {
      const response = await fetch('/api/issues/search', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query: query.raw })
      });
      const data = await response.json();
      setIssues(data.issues);
    } catch (error) {
      console.error('Search failed:', error);
    } finally {
      setLoading(false);
    }
  };

  const getUsers = async (field: string, search: string) => {
    if (field !== 'assignee') return [];

    const response = await fetch(`/api/users?search=${search}`);
    const users = await response.json();
    return users.map(user => ({
      value: user.id,
      label: `${user.name} (${user.email})`
    }));
  };

  return (
    <div>
      <QLInput
        config={issueConfig}
        placeholder='Search issues... e.g., status = "open" AND assignee = currentUser()'
        onExecute={handleSearch}
        getAsyncValueSuggestions={getUsers}
      />

      {loading && <div>Searching...</div>}

      <div>
        {issues.map(issue => (
          <div key={issue.id} className="issue-card">
            <h3>{issue.title}</h3>
            <p>Status: {issue.status}</p>
            <p>Assignee: {issue.assignee}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

E-commerce Product Search

import React from 'react';
import { QLInput } from '@abaktiar/ql-input';

const productConfig = {
  fields: [
    {
      name: 'name',
      displayName: 'Product Name',
      type: 'text',
      operators: ['~', '!~', 'IS EMPTY', 'IS NOT EMPTY']
    },
    {
      name: 'category',
      displayName: 'Category',
      type: 'option',
      operators: ['=', '!=', 'IN', 'NOT IN'],
      options: [
        { value: 'electronics', label: 'Electronics' },
        { value: 'clothing', label: 'Clothing' },
        { value: 'books', label: 'Books' }
      ]
    },
    {
      name: 'price',
      displayName: 'Price',
      type: 'number',
      operators: ['=', '>', '<', '>=', '<='],
      sortable: true
    },
    {
      name: 'inStock',
      displayName: 'In Stock',
      type: 'boolean',
      operators: ['=', '!=']
    }
  ],
  allowOrderBy: true,
  allowParentheses: true
};

function ProductSearch() {
  const handleProductSearch = (value: string, query: QLQuery) => {
    console.log('Searching products:', query);
    // Implement product search logic
  };

  return (
    <QLInput
      config={productConfig}
      placeholder='Search products... e.g., category = "electronics" AND price < 500 ORDER BY price ASC'
      onChange={handleProductSearch}
    />
  );
}

🎨 Icon Customization

Icon Visibility Control

Control which icons are displayed in the input:

// Hide search icon for minimal design
<QLInput
  config={config}
  showSearchIcon={false}
  placeholder="Enter query..."
/>

// Hide clear icon to prevent accidental clearing
<QLInput
  config={config}
  showClearIcon={false}
  placeholder="Enter query..."
/>

// Hide both icons for ultra-minimal design
<QLInput
  config={config}
  showSearchIcon={false}
  showClearIcon={false}
  placeholder="Enter query..."
/>

// Default behavior (both icons shown)
<QLInput
  config={config}
  showSearchIcon={true}  // Default
  showClearIcon={true}   // Default
  placeholder="Enter query..."
/>

Use Cases for Icon Control

  • Minimal Design: Hide search icon when the context is clear
  • Prevent Accidental Clearing: Hide clear icon in critical forms
  • Mobile Optimization: Reduce visual clutter on small screens
  • Custom Branding: Match your application's design system

🎨 Theming Examples

Custom Theme

/* Custom theme variables */
.my-custom-theme {
  --ql-input-border: #e2e8f0;
  --ql-input-background: #ffffff;
  --ql-input-foreground: #1a202c;
  --ql-input-muted: #f7fafc;
  --ql-input-muted-foreground: #718096;
  --ql-input-ring: #3182ce;
}

.my-custom-theme .ql-input {
  border-radius: 12px;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

.my-custom-theme .ql-suggestion-item {
  padding: 12px;
  border-radius: 8px;
}

.my-custom-theme .ql-suggestion-item:hover {
  background-color: #ebf8ff;
}

Material Design Theme

.material-theme {
  --ql-input-border: #e0e0e0;
  --ql-input-background: #ffffff;
  --ql-input-foreground: #212121;
  --ql-input-muted: #f5f5f5;
  --ql-input-muted-foreground: #757575;
  --ql-input-ring: #2196f3;
}

.material-theme .ql-input {
  border-radius: 4px;
  border-bottom: 2px solid var(--ql-input-ring);
  border-top: none;
  border-left: none;
  border-right: none;
  background: transparent;
}

🔧 Performance Optimization

Debounced Async Suggestions

import React, { useMemo } from 'react';
import { QLInput, useDebounce } from '@abaktiar/ql-input';

function OptimizedComponent() {
  const debouncedGetSuggestions = useMemo(
    () => useDebounce(async (field: string, search: string) => {
      // Expensive API call
      const response = await fetch(`/api/suggestions/${field}?q=${search}`);
      return response.json();
    }, 300),
    []
  );

  return (
    <QLInput
      config={config}
      getAsyncValueSuggestions={debouncedGetSuggestions}
    />
  );
}

Memoized Configuration

import React, { useMemo } from 'react';
import { QLInput } from '@abaktiar/ql-input';

function MemoizedConfig({ fields, options }) {
  const config = useMemo(() => ({
    fields: fields.map(field => ({
      ...field,
      options: options[field.name] || []
    })),
    maxSuggestions: 10,
    allowParentheses: true,
    allowOrderBy: true
  }), [fields, options]);

  return <QLInput config={config} />;
}

🧪 Testing Examples

Complete Test Suite

import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QLInput } from '@abaktiar/ql-input';

const testConfig = {
  fields: [
    {
      name: 'status',
      displayName: 'Status',
      type: 'option',
      operators: ['=', '!='],
      options: [
        { value: 'open', label: 'Open' },
        { value: 'closed', label: 'Closed' }
      ]
    }
  ]
};

describe('QLInput Component', () => {
  test('shows suggestions when typing', async () => {
    render(<QLInput config={testConfig} />);

    const input = screen.getByRole('textbox');
    await userEvent.type(input, 'sta');

    await waitFor(() => {
      expect(screen.getByText('Status')).toBeInTheDocument();
    });
  });

  test('executes query on Enter', async () => {
    const handleExecute = jest.fn();
    render(<QLInput config={testConfig} onExecute={handleExecute} />);

    const input = screen.getByRole('textbox');
    await userEvent.type(input, 'status = "open"');
    await userEvent.keyboard('{Enter}');

    expect(handleExecute).toHaveBeenCalledWith(
      expect.objectContaining({
        raw: 'status = "open"',
        valid: true
      })
    );
  });

  test('handles async suggestions', async () => {
    const getAsyncSuggestions = jest.fn().mockResolvedValue([
      { value: 'user1', label: 'User 1' }
    ]);

    render(
      <QLInput
        config={testConfig}
        getAsyncValueSuggestions={getAsyncSuggestions}
      />
    );

    const input = screen.getByRole('textbox');
    await userEvent.type(input, 'assignee = "u');

    await waitFor(() => {
      expect(getAsyncSuggestions).toHaveBeenCalledWith('assignee', 'u');
    });
  });
});

🐛 Issues & Support

Please report issues on GitHub Issues.