Skip to content

RowSelectionCheckbox

Flexible checkbox component for row selection with master/detail checkbox support.

Overview

The RowSelectionCheckbox component manages row selection state. It supports both individual row selection and a master "select all" checkbox with indeterminate state support.

Best for: Implementing row selection UI in tables and lists

Key Features:

  • ✅ Individual row selection
  • ✅ Master checkbox with indeterminate state
  • ✅ Select all / deselect all functionality
  • ✅ Full keyboard support
  • ✅ Accessibility labels (ARIA)
  • ✅ Disabled state support

Props

PropTypeDefaultRequiredDescription
rowIdstring-✅ YesUnique identifier for this row (use "master" for the master checkbox)
isSelectedbooleanfalse✅ YesWhether this checkbox is checked
isMasterbooleanfalse-Whether this is the master "select all" checkbox
disabledbooleanfalse-Whether the checkbox is disabled
totalRowsnumber--Total number of rows (used for master checkbox indeterminate calc)
selectedCountnumber--Number of selected rows (used for master checkbox indeterminate state)

Events

EventPayloadDescription
toggle[rowId: string, isSelected: boolean]Individual row checkbox toggled
toggle-all[isSelected: boolean]Master checkbox toggled (select all)

Basic Usage

Individual Row Selection

vue
<template>
  <div class="table">
    <div v-for="row in rows" :key="row.id" class="row">
      <RowSelectionCheckbox
        :row-id="String(row.id)"
        :is-selected="selectedRows.has(row.id)"
        @toggle="(rowId, isSelected) => toggleRow(rowId, isSelected)"
      />
      <span>{{ row.name }}</span>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import RowSelectionCheckbox from '@/components/Table/RowSelectionCheckbox.vue'

const rows = ref([
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' },
  { id: 3, name: 'Item 3' }
])

const selectedRows = ref(new Set<number>())

function toggleRow(rowId: string, isSelected: boolean) {
  const id = parseInt(rowId)
  if (isSelected) {
    selectedRows.value.add(id)
  } else {
    selectedRows.value.delete(id)
  }
}
</script>

Master Checkbox (Select All)

vue
<template>
  <div>
    <!-- Master checkbox in header -->
    <RowSelectionCheckbox
      row-id="master"
      :is-master="true"
      :is-selected="allSelected"
      :total-rows="rows.length"
      :selected-count="selectedRows.size"
      @toggle-all="toggleSelectAll"
    />

    <!-- Individual row checkboxes -->
    <div v-for="row in rows" :key="row.id" class="row">
      <RowSelectionCheckbox
        :row-id="String(row.id)"
        :is-selected="selectedRows.has(row.id)"
        @toggle="(rowId, selected) => toggleRow(rowId, selected)"
      />
      <span>{{ row.name }}</span>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import RowSelectionCheckbox from '@/components/Table/RowSelectionCheckbox.vue'

const rows = ref([
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' },
  { id: 3, name: 'Item 3' }
])

const selectedRows = ref(new Set<number>())

const allSelected = computed(() => selectedRows.value.size === rows.value.length)

function toggleSelectAll(isSelected: boolean) {
  selectedRows.value.clear()
  if (isSelected) {
    rows.value.forEach(row => selectedRows.value.add(row.id))
  }
}

function toggleRow(rowId: string, isSelected: boolean) {
  const id = parseInt(rowId)
  if (isSelected) {
    selectedRows.value.add(id)
  } else {
    selectedRows.value.delete(id)
  }
}
</script>

Master Checkbox Indeterminate State

The master checkbox shows three states:

  1. Unchecked - No rows selected
  2. Indeterminate - Some rows selected (✓ but not filled)
  3. Checked - All rows selected
typescript
// The component automatically calculates indeterminate state
// when totalRows and selectedCount are provided

// Unchecked: selectedCount = 0
<RowSelectionCheckbox
  row-id="master"
  :is-master="true"
  :is-selected="false"
  :total-rows="10"
  :selected-count="0"
/>

// Indeterminate: selectedCount > 0 && selectedCount < totalRows
<RowSelectionCheckbox
  row-id="master"
  :is-master="true"
  :is-selected="true"  // Still true even in indeterminate
  :total-rows="10"
  :selected-count="5"  // Shows indeterminate visually
/>

// Checked: selectedCount = totalRows
<RowSelectionCheckbox
  row-id="master"
  :is-master="true"
  :is-selected="true"
  :total-rows="10"
  :selected-count="10"
/>

Accessibility

Aria Labels

Component automatically generates appropriate ARIA labels:

typescript
// Individual row
aria-label="Select row 123"

// Master checkbox
aria-label="Select all rows"

Keyboard Support

  • Tab/Shift+Tab - Navigate to checkbox
  • Space/Enter - Toggle checkbox
  • Full keyboard navigation - No mouse required

Disabled State

vue
<template>
  <!-- Disabled individual checkbox -->
  <RowSelectionCheckbox
    row-id="123"
    :is-selected="false"
    :disabled="true"
  />

  <!-- Disabled master checkbox -->
  <RowSelectionCheckbox
    row-id="master"
    :is-master="true"
    :is-selected="false"
    :disabled="true"
  />
</template>

Advanced Examples

With Pagination

Handle selection across paginated data:

vue
<script setup lang="ts">
const currentPageRows = ref([...])
const selectedRows = ref(new Set())

// Track selections across all pages
const pageSelectionsMap = ref(new Map())

function toggleRow(rowId: string, isSelected: boolean) {
  if (isSelected) {
    selectedRows.value.add(rowId)
  } else {
    selectedRows.value.delete(rowId)
  }
  // Save current page selections
  pageSelectionsMap.value.set(currentPage.value, selectedRows.value)
}

function onPageChange(newPage) {
  currentPage.value = newPage
  // Restore selections for new page
  selectedRows.value = pageSelectionsMap.value.get(newPage) || new Set()
}
</script>

Conditional Selection

Disable selection for certain rows:

vue
<template>
  <RowSelectionCheckbox
    :row-id="String(row.id)"
    :is-selected="selectedRows.has(row.id)"
    :disabled="!isSelectable(row)"
    @toggle="(rowId, selected) => toggleRow(rowId, selected)"
  />
</template>

<script setup lang="ts">
function isSelectable(row: any): boolean {
  // Can't select archived rows
  return row.status !== 'archived'
}
</script>

Real-World Example: Invoice Selection

This example shows how to use RowSelectionCheckbox with realistic invoice data to select multiple invoices for bulk approval operations.

Data Model

typescript
interface Invoice {
  id: string
  documentNumber: string
  vendor: string
  amount: number
  currency: string
  invoiceDate: string
  status: 'pending' | 'approved' | 'rejected' | 'processed'
  assignee: string
  extractionConfidence: number
}

const invoices = [
  {
    id: 'INV-2024-001',
    documentNumber: 'INV-001234',
    vendor: 'Acme Corporation',
    amount: 1250.00,
    currency: 'USD',
    invoiceDate: '2024-01-15',
    status: 'pending',
    assignee: 'john.doe@company.com',
    extractionConfidence: 0.98
  },
  {
    id: 'INV-2024-002',
    documentNumber: 'INV-001235',
    vendor: 'Global Supplies Inc',
    amount: 3750.50,
    currency: 'USD',
    invoiceDate: '2024-01-16',
    status: 'pending',
    assignee: 'jane.smith@company.com',
    extractionConfidence: 0.95
  },
  {
    id: 'INV-2024-003',
    documentNumber: 'RE-2024-789',
    vendor: 'Tech Solutions GmbH',
    amount: 2100.00,
    currency: 'EUR',
    invoiceDate: '2024-01-18',
    status: 'pending',
    assignee: 'mike.johnson@company.com',
    extractionConfidence: 0.97
  },
  {
    id: 'INV-2024-004',
    documentNumber: 'INV-UK-445',
    vendor: 'British Office Ltd',
    amount: 890.00,
    currency: 'GBP',
    invoiceDate: '2024-01-20',
    status: 'pending',
    assignee: 'sarah.williams@company.com',
    extractionConfidence: 0.89
  },
  {
    id: 'INV-2024-005',
    documentNumber: 'F-2024-1122',
    vendor: 'Consulting Partners SA',
    amount: 8500.00,
    currency: 'EUR',
    invoiceDate: '2024-01-22',
    status: 'pending',
    assignee: 'pierre.dubois@company.com',
    extractionConfidence: 0.93
  },
  {
    id: 'INV-2024-006',
    documentNumber: 'INV-2024-556',
    vendor: 'Cloud Services Corp',
    amount: 4200.00,
    currency: 'USD',
    invoiceDate: '2024-01-25',
    status: 'approved',
    assignee: 'john.doe@company.com',
    extractionConfidence: 0.99
  },
  {
    id: 'INV-2024-007',
    documentNumber: 'BILL-7788',
    vendor: 'Utilities Company',
    amount: 650.25,
    currency: 'USD',
    invoiceDate: '2024-01-28',
    status: 'approved',
    assignee: 'accounts@company.com',
    extractionConfidence: 0.96
  },
  {
    id: 'INV-2024-008',
    documentNumber: 'INV-Q1-2024',
    vendor: 'Marketing Agency LLC',
    amount: 12000.00,
    currency: 'USD',
    invoiceDate: '2024-01-30',
    status: 'pending',
    assignee: 'marketing@company.com',
    extractionConfidence: 0.91
  }
]

Implementation with Master Checkbox

vue
<template>
  <div class="invoice-selection">
    <!-- Header with master checkbox -->
    <div class="selection-header">
      <RowSelectionCheckbox
        row-id="master"
        :is-master="true"
        :is-selected="allInvoicesSelected"
        :total-rows="invoices.length"
        :selected-count="selectedInvoices.size"
        @toggle-all="toggleSelectAllInvoices"
      />
      <span class="header-label">Select Invoices ({{ selectedInvoices.size }} selected)</span>
      <button
        v-if="selectedInvoices.size > 0"
        class="bulk-approve-btn"
        @click="approveSelectedInvoices"
      >
        Approve Selected
      </button>
    </div>

    <!-- Invoice rows with individual checkboxes -->
    <div class="invoice-list">
      <div v-for="invoice in invoices" :key="invoice.id" class="invoice-row">
        <RowSelectionCheckbox
          :row-id="invoice.id"
          :is-selected="selectedInvoices.has(invoice.id)"
          :disabled="invoice.status === 'approved'"
          @toggle="(rowId, selected) => toggleInvoice(rowId, selected)"
        />
        <div class="invoice-details">
          <div class="vendor-info">
            <strong>{{ invoice.vendor }}</strong>
            <span class="doc-number">{{ invoice.documentNumber }}</span>
          </div>
          <div class="amount-info">
            {{ invoice.currency }} {{ invoice.amount.toFixed(2) }}
          </div>
          <div class="meta-info">
            <span>{{ new Date(invoice.invoiceDate).toLocaleDateString() }}</span>
            <span :class="`status status-${invoice.status}`">
              {{ invoice.status.replace('_', ' ').toUpperCase() }}
            </span>
            <span class="confidence">{{ (invoice.extractionConfidence * 100).toFixed(0) }}% confidence</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import RowSelectionCheckbox from '@/components/Table/RowSelectionCheckbox.vue'

const invoices = ref([
  // ... invoice data from above ...
])

const selectedInvoices = ref(new Set<string>())

const allInvoicesSelected = computed(() => {
  const selectableCount = invoices.value.filter(inv => inv.status !== 'approved').length
  return selectedInvoices.value.size === selectableCount && selectableCount > 0
})

function toggleSelectAllInvoices(isSelected: boolean) {
  selectedInvoices.value.clear()
  if (isSelected) {
    invoices.value.forEach(invoice => {
      // Only select non-approved invoices
      if (invoice.status !== 'approved') {
        selectedInvoices.value.add(invoice.id)
      }
    })
  }
}

function toggleInvoice(invoiceId: string, isSelected: boolean) {
  if (isSelected) {
    selectedInvoices.value.add(invoiceId)
  } else {
    selectedInvoices.value.delete(invoiceId)
  }
}

function approveSelectedInvoices() {
  const selected = Array.from(selectedInvoices.value)
  console.log('Approving invoices:', selected)
  // Make API call to approve selected invoices
  // Then clear selection
  selectedInvoices.value.clear()
}
</script>

<style scoped>
.invoice-selection {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
}

.selection-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
  background: #f5f5f5;
  border-bottom: 1px solid #e0e0e0;
}

.header-label {
  flex: 1;
  font-weight: 500;
}

.bulk-approve-btn {
  padding: 8px 16px;
  background: #2388AE;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 500;
}

.invoice-list {
  max-height: 500px;
  overflow-y: auto;
}

.invoice-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  border-bottom: 1px solid #e0e0e0;
  transition: background 0.2s;
}

.invoice-row:hover {
  background: #fafafa;
}

.invoice-details {
  flex: 1;
}

.vendor-info {
  display: flex;
  gap: 12px;
  align-items: baseline;
  margin-bottom: 4px;
}

.doc-number {
  color: #666;
  font-size: 0.9em;
}

.amount-info {
  font-weight: 600;
  font-size: 1.1em;
  margin-bottom: 4px;
}

.meta-info {
  display: flex;
  gap: 16px;
  font-size: 0.85em;
  color: #666;
}

.status {
  padding: 2px 8px;
  border-radius: 3px;
  font-size: 0.75em;
  font-weight: 600;
}

.status-pending {
  background: #fff3cd;
  color: #856404;
}

.status-approved {
  background: #d4edda;
  color: #155724;
}

.confidence {
  color: #2388AE;
}
</style>

What This Example Shows

  • Master Checkbox: Select/deselect all pending invoices at once
  • Conditional Selection: Approved invoices cannot be selected (disabled state)
  • Indeterminate State: Master checkbox shows partial selection state
  • Bulk Actions: Display action button when invoices are selected
  • Real Data: Mix of currencies (USD, EUR, GBP) and statuses
  • Selection Count: Dynamic feedback showing number of selected items

Real-World Example: Document Queue Selection

This example demonstrates selecting multiple documents from a processing queue for batch operations.

Data Model

typescript
interface Document {
  id: string
  fileName: string
  documentType: 'Invoice' | 'PO' | 'Receipt' | 'Contract'
  uploadDate: string
  pages: number
  processingStatus: 'queued' | 'processing' | 'completed' | 'failed'
  ocrConfidence: number
  extractedFields: number
}

const documents = [
  {
    id: 'DOC-2024-001',
    fileName: 'invoice_acme_jan2024.pdf',
    documentType: 'Invoice',
    uploadDate: '2024-01-15T10:30:00Z',
    pages: 3,
    processingStatus: 'completed',
    ocrConfidence: 0.96,
    extractedFields: 15
  },
  {
    id: 'DOC-2024-002',
    fileName: 'po_global_supply.pdf',
    documentType: 'PO',
    uploadDate: '2024-01-16T14:22:00Z',
    pages: 2,
    processingStatus: 'completed',
    ocrConfidence: 0.98,
    extractedFields: 12
  },
  {
    id: 'DOC-2024-003',
    fileName: 'receipt_office_depot.jpg',
    documentType: 'Receipt',
    uploadDate: '2024-01-17T09:15:00Z',
    pages: 1,
    processingStatus: 'completed',
    ocrConfidence: 0.92,
    extractedFields: 8
  },
  {
    id: 'DOC-2024-004',
    fileName: 'contract_tech_solutions.pdf',
    documentType: 'Contract',
    uploadDate: '2024-01-18T16:45:00Z',
    pages: 5,
    processingStatus: 'processing',
    ocrConfidence: 0.0,
    extractedFields: 0
  },
  {
    id: 'DOC-2024-005',
    fileName: 'invoice_cloud_services.pdf',
    documentType: 'Invoice',
    uploadDate: '2024-01-19T11:20:00Z',
    pages: 1,
    processingStatus: 'completed',
    ocrConfidence: 0.99,
    extractedFields: 14
  },
  {
    id: 'DOC-2024-006',
    fileName: 'po_british_office.pdf',
    documentType: 'PO',
    uploadDate: '2024-01-20T13:30:00Z',
    pages: 3,
    processingStatus: 'queued',
    ocrConfidence: 0.0,
    extractedFields: 0
  },
  {
    id: 'DOC-2024-007',
    fileName: 'receipt_utilities.pdf',
    documentType: 'Receipt',
    uploadDate: '2024-01-21T08:00:00Z',
    pages: 1,
    processingStatus: 'failed',
    ocrConfidence: 0.45,
    extractedFields: 3
  },
  {
    id: 'DOC-2024-008',
    fileName: 'invoice_consulting_partners.pdf',
    documentType: 'Invoice',
    uploadDate: '2024-01-22T15:10:00Z',
    pages: 2,
    processingStatus: 'completed',
    ocrConfidence: 0.91,
    extractedFields: 13
  }
]

Implementation with Batch Operations

vue
<template>
  <div class="document-queue">
    <!-- Header with master checkbox -->
    <div class="queue-header">
      <RowSelectionCheckbox
        row-id="master"
        :is-master="true"
        :is-selected="allDocumentsSelected"
        :total-rows="selectableDocuments.length"
        :selected-count="selectedDocuments.size"
        @toggle-all="toggleSelectAllDocuments"
      />
      <span class="header-label">Processing Queue ({{ selectedDocuments.size }} selected)</span>
      <div v-if="selectedDocuments.size > 0" class="batch-actions">
        <button class="action-btn export" @click="exportSelectedDocuments">
          📥 Export
        </button>
        <button class="action-btn delete" @click="deleteSelectedDocuments">
          🗑️ Delete
        </button>
      </div>
    </div>

    <!-- Document queue rows -->
    <div class="document-queue-list">
      <div v-for="doc in documents" :key="doc.id" class="document-row">
        <RowSelectionCheckbox
          :row-id="doc.id"
          :is-selected="selectedDocuments.has(doc.id)"
          :disabled="doc.processingStatus === 'processing'"
          @toggle="(rowId, selected) => toggleDocument(rowId, selected)"
        />
        <div class="document-icon">
          {{ getDocumentTypeIcon(doc.documentType) }}
        </div>
        <div class="document-info">
          <div class="file-name">{{ doc.fileName }}</div>
          <div class="file-meta">
            <span class="type">{{ doc.documentType }}</span>
            <span class="pages">{{ doc.pages }} page(s)</span>
            <span class="uploaded">{{ formatDate(doc.uploadDate) }}</span>
          </div>
        </div>
        <div class="status-indicator">
          <div :class="`status-badge ${doc.processingStatus}`">
            {{ formatStatus(doc.processingStatus) }}
          </div>
          <div v-if="doc.processingStatus === 'completed'" class="confidence">
            {{ (doc.ocrConfidence * 100).toFixed(0) }}% confidence
          </div>
          <div v-else-if="doc.processingStatus === 'processing'" class="processing">
            Processing...
          </div>
          <div v-else-if="doc.processingStatus === 'failed'" class="failed">
            {{ doc.extractedFields }} fields extracted
          </div>
          <div v-else class="queued">
            Queued
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import RowSelectionCheckbox from '@/components/Table/RowSelectionCheckbox.vue'

const documents = ref([
  // ... document data from above ...
])

const selectedDocuments = ref(new Set<string>())

const selectableDocuments = computed(() =>
  documents.value.filter(doc => doc.processingStatus !== 'processing')
)

const allDocumentsSelected = computed(() => {
  return selectedDocuments.value.size === selectableDocuments.value.length &&
         selectableDocuments.value.length > 0
})

function toggleSelectAllDocuments(isSelected: boolean) {
  selectedDocuments.value.clear()
  if (isSelected) {
    selectableDocuments.value.forEach(doc => {
      selectedDocuments.value.add(doc.id)
    })
  }
}

function toggleDocument(docId: string, isSelected: boolean) {
  if (isSelected) {
    selectedDocuments.value.add(docId)
  } else {
    selectedDocuments.value.delete(docId)
  }
}

function exportSelectedDocuments() {
  const selected = Array.from(selectedDocuments.value)
  console.log('Exporting documents:', selected)
  // Make API call to export
  selectedDocuments.value.clear()
}

function deleteSelectedDocuments() {
  const selected = Array.from(selectedDocuments.value)
  if (confirm(`Delete ${selected.length} document(s)?`)) {
    console.log('Deleting documents:', selected)
    // Make API call to delete
    selectedDocuments.value.clear()
  }
}

function getDocumentTypeIcon(type: string): string {
  const icons: Record<string, string> = {
    Invoice: '📄',
    PO: '📋',
    Receipt: '🧾',
    Contract: '📑'
  }
  return icons[type] || '📄'
}

function formatDate(dateString: string): string {
  return new Date(dateString).toLocaleDateString()
}

function formatStatus(status: string): string {
  return status.charAt(0).toUpperCase() + status.slice(1).replace('_', ' ')
}
</script>

<style scoped>
.document-queue {
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
}

.queue-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
  background: #f5f5f5;
  border-bottom: 1px solid #e0e0e0;
}

.header-label {
  flex: 1;
  font-weight: 500;
}

.batch-actions {
  display: flex;
  gap: 8px;
}

.action-btn {
  padding: 6px 12px;
  border: none;
  border-radius: 4px;
  font-size: 0.9em;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s;
}

.action-btn.export {
  background: #34eea9;
  color: white;
}

.action-btn.delete {
  background: #ff6b6b;
  color: white;
}

.document-queue-list {
  max-height: 600px;
  overflow-y: auto;
}

.document-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  border-bottom: 1px solid #e0e0e0;
  transition: background 0.2s;
}

.document-row:hover {
  background: #fafafa;
}

.document-icon {
  font-size: 1.5em;
  width: 32px;
  text-align: center;
}

.document-info {
  flex: 1;
}

.file-name {
  font-weight: 500;
  margin-bottom: 4px;
}

.file-meta {
  display: flex;
  gap: 12px;
  font-size: 0.85em;
  color: #666;
}

.status-indicator {
  text-align: right;
  min-width: 150px;
}

.status-badge {
  display: inline-block;
  padding: 4px 8px;
  border-radius: 3px;
  font-size: 0.85em;
  font-weight: 600;
  margin-bottom: 4px;
}

.status-badge.completed {
  background: #d4edda;
  color: #155724;
}

.status-badge.queued {
  background: #e2e3e5;
  color: #383d41;
}

.status-badge.processing {
  background: #fff3cd;
  color: #856404;
}

.status-badge.failed {
  background: #f8d7da;
  color: #721c24;
}

.confidence {
  font-size: 0.85em;
  color: #2388AE;
  font-weight: 500;
}

.processing,
.queued {
  font-size: 0.85em;
  color: #666;
}

.failed {
  font-size: 0.85em;
  color: #ff6b6b;
}
</style>

What This Example Shows

  • Document Types: Icons and labels for different document types (Invoice, PO, Receipt, Contract)
  • Processing States: Different visual feedback for queued, processing, completed, and failed documents
  • Disabled During Processing: Cannot select documents that are actively being processed
  • Batch Export/Delete: Quick access to batch operations when documents are selected
  • OCR Quality Metrics: Display confidence scores and extracted field counts
  • Real Data: Realistic file names and timestamps
  • Selective Activation: Master checkbox only counts selectable documents (excludes processing ones)

Testing

Unit Test Example

typescript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import RowSelectionCheckbox from './RowSelectionCheckbox.vue'

describe('RowSelectionCheckbox', () => {
  it('renders unchecked by default', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false
      }
    })

    const checkbox = wrapper.find('input[type="checkbox"]')
    expect(checkbox.element).not.toBeChecked()
  })

  it('emits toggle event when clicked', async () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: '1',
        isSelected: false
      }
    })

    const checkbox = wrapper.find('input')
    await checkbox.trigger('click')

    expect(wrapper.emitted('toggle')).toBeTruthy()
    expect(wrapper.emitted('toggle')[0]).toEqual(['1', true])
  })

  it('shows indeterminate state correctly', () => {
    const wrapper = mount(RowSelectionCheckbox, {
      props: {
        rowId: 'master',
        isMaster: true,
        isSelected: true,
        totalRows: 10,
        selectedCount: 5
      }
    })

    const checkbox = wrapper.find('input')
    expect(checkbox.element).toHaveProperty('indeterminate', true)
  })
})

Browser Support

Supported in all modern browsers:

  • Chrome 88+
  • Firefox 78+
  • Safari 14+
  • Edge 88+

Source Code

  • Component: src/components/Table/RowSelectionCheckbox.vue (83 lines)
  • Tests: 42 component tests, 30+ unit tests

Changelog

v1.0.0 (2025-12-18)

  • Initial release
  • Individual and master checkbox support
  • Indeterminate state for partial selection
  • Full accessibility support
  • Keyboard navigation

DocBits Component Library