1.5.0 โ€ข Published 4 months ago

@abaktiar/ql-input v1.5.0

Weekly downloads
-
License
MIT
Repository
github
Last release
4 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

4 months ago

1.4.0

5 months ago

1.3.1

5 months ago

1.3.0

5 months ago

1.2.0

5 months ago

1.1.0

5 months ago

1.0.1

5 months ago

1.0.0

5 months ago