1.5.0 โ€ข Published 10 months ago

@abaktiar/ql-input v1.5.0

Weekly downloads
-
License
MIT
Repository
github
Last release
10 months ago

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

1.5.0

10 months ago

1.4.0

10 months ago

1.3.1

10 months ago

1.3.0

10 months ago

1.2.0

10 months ago

1.1.0

10 months ago

1.0.1

10 months ago

1.0.0

10 months ago