nb-input-search

This is a search field component for Vue.js 3+.

Typing debounce uses @vlalg-nimbus/magic-debounce, included as a dependency of @vlalg-nimbus/nb-inputs.

Loading component...

Installation

Yarn
yarn add @vlalg-nimbus/nb-inputs
NPM
npm install @vlalg-nimbus/nb-inputs

Usage

Vue 3
import { createApp } from 'vue'
import App from './App.vue'

import NbInputComponents from '@vlalg-nimbus/nb-inputs'
import "@vlalg-nimbus/nb-inputs/dist/style.css";

const app = createApp(App)
app.use(NbInputComponents)
app.mount('#app')
Nuxt 3
import NbInputComponents from '@vlalg-nimbus/nb-inputs'
import "@vlalg-nimbus/nb-inputs/dist/style.css";

export default defineNuxtPlugin(context => {
  context.vueApp.use(NbInputComponents)
})

To use, simply call the component, in this case it will be NbInputSearch or nb-input-search.

Mode 1
<template>
  <NbInputSearch />
</template>
Mode 2
<template>
  <nb-input-search />
</template>
Mode 3
<template>
  <nb-input-search></nb-input-search>
</template>

Preview & Playground

Select the component you want to edit/test

Loading Sandbox...

Props

Items with an (*) mean they are required

nameValue typeDefaultDescription
nbId (*)StringRoot id for the field.
displayString'b'Display mode: b or ib.
tabIndexNumber0Tab index for keyboard focus.
hasTabIndexEnterBooleantrueWhen true, Enter runs the submit path (emit entered + interactionFunction when allowed).
ariaLabelString'Alternate Text Button'aria-label for the control / button context.
ariaAttrsObject{}Extra ARIA attributes (keys get aria- prefix).
titleString''Native tooltip on hover.
textColorString'#ffffff'Main text color (hex or CSS color).
caretColorString''Caret color; empty uses browser default.
selectionBgColorString''Selection background; empty uses browser default.
selectionTextColorString''Selected text color; empty uses browser default.
themeString'light'light or dark.
hasBorderRadiusBooleanfalseEnables border radius on the control.
hasBorderFocusBooleantrueWhen true, applies a highlight border on the input while focused (light-focus-active-border-color / dark-focus-active-border-color).
lightFocusActiveBorderColorString'#2563eb'Focus border when theme="light" (light-focus-active-border-color).
darkFocusActiveBorderColorString'#7dd3fc'Focus border when theme="dark" (dark-focus-active-border-color).
borderRadiusNumber0.5Border radius value.
disabledBooleanfalseDisables the field.
fontFamilyString'Lato', sans-serifInput font-family.
fontSizeStringnullInput font-size; when null, size follows sizeMediaQuery (same rules as nb-input).
fontWeightNumber400Input font-weight.
fontFamilyMsgString'Lato', sans-serifMessage font-family.
fontSizeMsgString'1em'Message font-size.
fontWeightMsgNumber400Message font-weight.
textMessageColorString'#f15574'Message text color.
textAlignString'left'left, center, or right.
inputTextString, NumbernullInitial value.
hasTrimBooleanfalseTrim resolved value before interactionFunction and related emits.
inputName (*)Stringname on the input / label for.
inputPaddingString'6px 10px'CSS padding for the inner input area.
inputPlaceholderString''Placeholder text.
activeTextStyleString'normal'normal, italic, or oblique when active.
sizeMediaQueryString'sm'Responsive size: xs, sm, md, lg.
requiredBooleanfalseHTML required when applicable.
inputReadonlyBooleanfalseRead-only input.
blockPasteBooleanfalseWhen true, blocks paste into the input (still emits paste).
inputAutocompleteString'on'on or off.
inputUppercaseBooleanfalseUppercase display/input behavior.
inputWidthNumber200Width in pixels.
inputStyleString'background'background, line, or border.
lightBgColorString'#f8f8f2'Light theme background.
lightBgColorFocusString'#eaeaea'Light theme background when focused.
lightControlBorderColorString'#334155'Light theme border.
lightControlBorderColorActiveString'#2563eb'Light theme border when active.
lightDisabledBgColorString'#dfdfd9'Light theme disabled background.
lightTextColorString'#0f172a'Light theme primary text.
darkBgColorString'#1e293b'Dark theme background.
darkBgColorFocusString'#334155'Dark theme background when focused.
darkControlBorderColorString'#94a3b8'Dark theme border.
darkControlBorderColorActiveString'#38bdf8'Dark theme border when active.
darkDisabledBgColorString'rgba(40, 42, 54, 1)'Dark theme disabled background.
darkTextColorString'#f1f5f9'Dark theme primary text.
darkDisabledControlBorderColorString'rgba(68, 71, 90, 0.3)'Dark theme disabled border.
lightDisabledControlBorderColorString'rgba(53, 55, 52, 0.3)'Light theme disabled border.
tabindexString, Number0Native tabindex on the input (see also tabIndex).
showMsgBooleanfalseShow validation / message area.
hasMsgBooleanfalseWhether a message should be considered present.
messageString'Default message text'Default message copy.
hasCustomMsgBooleanfalseUse the message slot instead of default markup.
extraContendAbsoluteBooleanfalseLay out message with position: absolute under the field (prop keeps Contend spelling).
hasIconBooleanfalseShow icon slot (left or right).
iconWidthNumber32Icon area width (px).
iconDirectionString'left'left or right. right applies only while the search button is hidden (debounce + interaction-debounce-wait > 0). If has-icon and the submit button are both visible, layout forces left.
iconPaddingString'5px 10px'Icon padding (two space-separated values).
iconPaddingInputNumber35Extra input padding on the icon side.
iconMarginString'0'Icon margin.
iconLightTextColorString'#f8fafc'Icon foreground (light).
iconDarkTextColorString'#f1f5f9'Icon foreground (dark).
iconLightBgColorString'#334155'Icon background (light).
iconLightBgColorActiveString'#475569'Icon background active (light).
iconDarkBgColorString'#334155'Icon background (dark).
iconDarkBgColorActiveString'#475569'Icon background active (dark).
iconDarkDisabledBgColorString'rgba(68, 71, 90, 0.3)'Icon disabled (dark).
iconLightDisabledBgColorString'rgba(53, 55, 52, 0.3)'Icon disabled (light).
iconBorderRadiusNumber0Icon border-radius.
iconSizeNumber1Icon scale multiplier.
showLabelBooleanfalseFloating label.
labelString'Label text'Label copy.
labelBreakOnActiveBooleantrueLabel line-break / ellipsis when active.
labelBackgroundString'transparent'Label background when floated.
labelPaddingString'1px 5px'Label padding when active.
labelBorderRadiusNumber0Label border radius.
labelLeftNumber5Label horizontal offset (inactive).
inputLabelMarginActiveNumber15Top margin when label is active.
labelActiveTopNumber-13Label top offset when active.
labelActiveLeftNumber5Label left offset when active.
labelRightNumber0Label right offset (inactive).
labelActiveRightNumber0Label right offset when active.
fontFamilyLabelString'Lato', sans-serifLabel font family.
fontSizeLabelString'1em'Label size when inactive.
fontSizeLabelActiveString'0.8em'Label size when active.
fontWeightLabelNumber400Label weight.
lightTextColorLabelString'#333333'Label color (light, inactive).
lightTextColorLabelActiveString'#333333'Label color (light, active).
darkTextColorLabelString'#ffffff'Label color (dark, inactive).
darkTextColorLabelActiveString'#ffffff'Label color (dark, active).
interactionFunctionFunctionasync () => {}Called with the resolved query string; may be async.
interactionDebounceWaitNumber0Debounce delay (ms) while typing; only when interaction-trigger="debounce" and > 0.
interactionTriggerString'submit'submit (only Enter / button) or debounce (typing debounce + Enter / button).
interactionDebounceMinLengthNumber2Minimum length (after has-trim) when min-length enforcement is on.
interactionDebounceEnforceMinLengthBooleanfalseWhen true and trigger is debounce, skips interactionFunction until length ≥ interactionDebounceMinLength; debounce cancel reason below-min-length.
submitButtonLabelString'search'Label on the submit control when the search button is visible (submit, or debounce with interactionDebounceWait <= 0). Maps to kebab-case submit-button-label.
lightSubmitButtonBgColorString'#1d4ed8'Button background when theme="light" (light-submit-button-bg-color).
darkSubmitButtonBgColorString'#38bdf8'Button background when theme="dark" (dark-submit-button-bg-color).
lightSubmitButtonBgColorHoverString'#1e40af'Hover background, theme="light" (light-submit-button-bg-color-hover).
darkSubmitButtonBgColorHoverString'#0ea5e9'Hover background, theme="dark" (dark-submit-button-bg-color-hover).
lightSubmitButtonTextColorString'#f8fafc'Button text when theme="light" (light-submit-button-text-color).
darkSubmitButtonTextColorString'#0f172a'Button text when theme="dark" (dark-submit-button-text-color).
lightSubmitButtonTextColorHoverString'#ffffff'Hover text, theme="light" (light-submit-button-text-color-hover).
darkSubmitButtonTextColorHoverString'#020617'Hover text, theme="dark" (dark-submit-button-text-color-hover).
submitButtonPaddingString'5px 10px'Search button padding (submit-button-padding).
submitButtonBorderRadiusString'0px'Search button border-radius (submit-button-border-radius).
submitButtonTopString'0'Search button top when absolutely positioned (submit-button-top).
submitButtonRightString'0'Search button right (submit-button-right).
submitButtonBottomString'0'Search button bottom (submit-button-bottom).
submitButtonFontFamilyString'Lato', sans-serifSearch button font-family (submit-button-font-family).
submitButtonFontSizeString'1.1em'Search button font-size (submit-button-font-size).
submitButtonFontWeightNumber700Search button font-weight (submit-button-font-weight).
submitButtonIsUppercaseBooleantrueUppercase label on the search button (submit-button-is-uppercase).

Events

nameReturn typeDescription
current-valuevalueFired when the value changes (trim rules apply like changed).
changedvalueFired when the value changes.
clearednothingFired when the value becomes empty after having had content (previous value was non-empty).
focusednothingInput focused.
blurrednothingInput blurred.
clickedMouseEventWrapper clicked.
enteredvalueFired when Enter / submit path runs, before interactionFunction.
pastestringPasted text; still fires if blockPaste blocks default paste.
interaction-start{ source, value }source: 'debounce' or 'submit'. Before interactionFunction.
interaction-end{ source, value }After interactionFunction resolves.
interaction-error{ source, value, error }interactionFunction threw or rejected.
interaction-cancel{ reason }reason: 'submit', 'clear-value', 'below-min-length', 'unmount', 'reconfigure'.

Slots

The component has slots for custom content:

icon

Only when hasIcon is true.

<template>
  <NbInputSearch
    nb-id="search-1"
    input-name="q"
    :has-icon="true"
    icon-direction="left"
  >
    <template #icon>
      <span>🔍</span>
    </template>
  </NbInputSearch>
</template>

message

When the message area is enabled (same flags as nb-input: show-msg, has-msg, has-custom-msg, etc.).

<template>
  <NbInputSearch
    nb-id="search-1"
    input-name="q"
    :show-msg="true"
    :has-msg="true"
    :has-custom-msg="true"
  >
    <template #message>
      <span>Helper or validation text</span>
    </template>
  </NbInputSearch>
</template>

Examples

Debounce on typing (500 ms) + trim + lifecycle events

interactionFunction runs after the user stops typing for interaction-debounce-wait ms. Enter (with hasTabIndexEnter) or a new keystroke resets the timer; Enter also runs the function immediately and emits entered first. The search button is hidden when debounce and wait > 0.

Template + script
<template>
  <NbInputSearch
    nb-id="search-debounce"
    display="b"
    label="Search (debounce)"
    input-name="search-debounce"
    input-style="border"
    input-text=""
    :has-trim="true"
    interaction-trigger="debounce"
    :interaction-debounce-wait="500"
    :interaction-function="onSearch"
    @entered="onEntered"
    @changed="lastChanged = $event"
    @interaction-start="onInteractionStart"
    @interaction-end="onInteractionEnd"
    @interaction-cancel="onInteractionCancel"
    @interaction-error="onInteractionError"
  />
  <p>Current value (<code>@changed</code>): {{ lastChanged || '(empty)' }}</p>
  <ul>
    <li v-for="row in log" :key="row.id">{{ row.text }}</li>
  </ul>
</template>

<script setup>
import { ref } from 'vue'

const lastChanged = ref('')
const log = ref([])

function push(kind, detail) {
  log.value = [
    { id: Date.now() + Math.random(), text: `${kind}: ${detail}` },
    ...log.value
  ].slice(0, 25)
}

const onSearch = async (query) => {
  push('interactionFunction', query)
  // await fetch('/api?q=' + encodeURIComponent(query))
}

const onEntered = (value) => push('@entered', value)
const onInteractionStart = (p) => push('@interaction-start', `${p.source} → ${p.value}`)
const onInteractionEnd = (p) => push('@interaction-end', `${p.source} → ${p.value}`)
const onInteractionCancel = (p) => push('@interaction-cancel', p.reason)
const onInteractionError = (p) => {
  const msg = p.error instanceof Error ? p.error.message : String(p.error)
  push('@interaction-error', `${p.source} → ${msg}`)
}
</script>

Submit + interaction-debounce-wait="0" (typical default)

Nothing runs while typing. interactionFunction runs on Enter (if hasTabIndexEnter) or search button click. The button is visible (submit always shows the button).

Submit wait 0
<template>
  <NbInputSearch
    nb-id="search-submit-zero"
    display="b"
    label="Search (Enter / button)"
    input-name="search-submit-zero"
    input-style="border"
    input-text=""
    interaction-trigger="submit"
    :interaction-debounce-wait="0"
    :interaction-function="onSearch"
    @entered="(v) => console.log('entered', v)"
  />
</template>

<script setup>
const onSearch = async (query) => {
  console.log('search', query)
}
</script>

Submit + interaction-debounce-wait="500"

Still no calls while typing: with interaction-trigger="submit", interaction-debounce-wait does not enable debounced typing. Only Enter / button run interactionFunction. Useful if you keep a single wait prop for toggling modes elsewhere.

Submit wait 500
<template>
  <NbInputSearch
    nb-id="search-submit-wait"
    display="b"
    label="Search (submit; wait ignored on type)"
    input-name="search-submit-wait"
    input-style="border"
    input-text=""
    interaction-trigger="submit"
    :interaction-debounce-wait="500"
    :interaction-function="onSearch"
    @entered="(v) => console.log('entered', v)"
  />
</template>

<script setup>
const onSearch = async (query) => {
  console.log('search', query)
}
</script>

Debounce + in-memory list (fake latency)

Filter an array of { id, text, ... } by text inside interactionFunction. A short setTimeout (or fetch) simulates network delay; swap the delay for a real API call when needed.

Fake API + results list
<template>
  <NbInputSearch
    nb-id="catalog-search"
    input-name="q"
    label="Search products"
    interaction-trigger="debounce"
    :interaction-debounce-wait="400"
    :has-trim="true"
    :interaction-function="runSearch"
    @changed="onChangedClear"
  />
  <ul v-if="results.length">
    <li v-for="item in results" :key="item.id">{{ item.text }} — {{ item.category }}</li>
  </ul>
  <p v-else-if="lastQuery !== ''">No matches for "{{ lastQuery }}".</p>
</template>

<script setup>
import { ref } from 'vue'

const CATALOG = [
  { id: '1', text: 'Wireless mouse', category: 'Peripherals' },
  { id: '2', text: 'USB hub', category: 'Accessories' },
  { id: '3', text: 'Mechanical keyboard', category: 'Peripherals' }
]

const results = ref([])
const lastQuery = ref('')

async function runSearch(query) {
  await new Promise((r) => setTimeout(r, 450))
  const q = String(query).trim().toLowerCase()
  lastQuery.value = String(query).trim()
  if (!q) {
    results.value = []
    return
  }
  results.value = CATALOG.filter((item) => item.text.toLowerCase().includes(q))
}

function onChangedClear(v) {
  if (!String(v ?? '').trim()) {
    results.value = []
    lastQuery.value = ''
  }
}
</script>

Debounce + fake API dropdown (clear naming)

Same behavior as the App.vue demo, with more descriptive variable/function names.

Template snippet
<div
  class="wrapper-field-search"
  ref="catalogResultsWrapperRef"
  @mouseleave="handleCatalogWrapperMouseLeave"
  @focusout="handleCatalogWrapperFocusOut"
>
  <NbInputSearch
    nb-id="search-demo-fake-catalog"
    display="b"
    label="Search product (fake API)"
    input-name="search-demo-fake-catalog"
    :input-text="catalogSearchInputValue"
    input-style="border"
    light-text-color="#ffffff"
    input-text=""
    :has-trim="true"
    interaction-trigger="debounce"
    :interaction-debounce-wait="400"
    :interaction-debounce-enforce-min-length="false"
    :interaction-debounce-min-length="3"
    :interaction-function="runCatalogSearch"
    @changed="handleCatalogSearchChanged"
    @cleared="handleCatalogSearchCleared"
    @interaction-start="handleCatalogInteractionStart"
    @blurred="handleCatalogSearchBlurred"
    @focused="handleCatalogSearchFocused"
  />

  <div v-if="isCatalogResultsVisible" class="wrapper-field-search__results" @click="closeCatalogResults">
    <div v-if="isCatalogLoading">
      <p>Carregando...</p>
    </div>
    <div v-else>
      <div v-if="filteredCatalogResults.length">
        <strong>Resultados ({{ filteredCatalogResults.length }})</strong>
        <ul>
          <li v-for="item in filteredCatalogResults" :key="item.id" @click.stop="handleCatalogResultSelect(item)">
            <strong>{{ item.text }}</strong>
            <span style="opacity: 0.75"> — {{ item.category }}</span>
          </li>
        </ul>
      </div>
      <p
        v-else
        style="margin: 0.75rem 0 0; font-size: 0.9em; opacity: 0.85"
      >
        Nenhum resultado para <code>{{ lastCatalogSearchQuery }}</code>.
      </p>
    </div>
  </div>
</div>
Script snippet
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'

const isCatalogLoading = ref(false)
const isCatalogResultsVisible = ref(false)

const catalogResultsWrapperRef = ref(null)

const hideCatalogResults = () => {
  isCatalogLoading.value = false
  isCatalogResultsVisible.value = false
}

const shouldCloseOnBlur = ref(false)
const handleCatalogWrapperMouseLeave = () => {
  if (!shouldCloseOnBlur.value) return

  hideCatalogResults()
}

const handleCatalogWrapperFocusOut = (event) => {
  if (!shouldCloseOnBlur.value) return

  const wrapper = catalogResultsWrapperRef.value
  const next = event?.relatedTarget || null

  // Se o foco ainda está dentro do wrapper, não esconde
  if (wrapper && next && wrapper.contains(next)) return

  hideCatalogResults()
}

const handleDocumentPointerDownOutsideCatalog = (event) => {
  const wrapper = catalogResultsWrapperRef.value
  if (!wrapper) return

  const target = event?.target
  if (target && wrapper.contains(target)) return

  hideCatalogResults()
}

onMounted(() => {
  document.addEventListener('pointerdown', handleDocumentPointerDownOutsideCatalog)
})

onUnmounted(() => {
  document.removeEventListener('pointerdown', handleDocumentPointerDownOutsideCatalog)
})

/** Static catalog for fake API: filter by `text` after debounce + simulated latency */
const CATALOG_ITEMS = [
  { id: '1', text: 'Notebook Pro 14', category: 'Computers' },
  { id: '2', text: 'Notebook Pro 15', category: 'Computers' },
  { id: '3', text: 'Notebook Pro 16 2025', category: 'Computers' },
  { id: '4', text: 'Notebook Pro 16 2026', category: 'Computers' },
  { id: '5', text: 'Notebook Pro 16 2027', category: 'Computers' },
  { id: '6', text: 'Notebook Pro 17 2026', category: 'Computers' },
  { id: '7', text: 'Wireless mouse', category: 'Peripherals' },
  { id: '8', text: 'Mechanical keyboard', category: 'Peripherals' },
  { id: '9', text: '27-inch monitor', category: 'Computers' },
  { id: '10', text: 'HD webcam', category: 'Peripherals' },
  { id: '11', text: 'Headset with microphone', category: 'Audio' },
  { id: '12', text: 'Bluetooth speaker', category: 'Audio' },
  { id: '13', text: '1TB SSD', category: 'Storage' },
  { id: '14', text: 'USB-C hub', category: 'Accessories' },
  { id: '15', text: 'Aluminum notebook base', category: 'Accessories' }
]

const filteredCatalogResults = ref([])
const lastCatalogSearchQuery = ref('')
const catalogSearchInputValue = ref('notebook')
const lastCatalogInteractionSource = ref('')
const catalogInteractionSourceCounters = ref({
  debounce: 0,
  submit: 0
})

const handleCatalogInteractionStart = (payload) => {
  const source = payload?.source === 'submit' ? 'submit' : 'debounce'
  lastCatalogInteractionSource.value = source
  catalogInteractionSourceCounters.value[source] += 1
}

/** Simulates network latency / backend filter (ms). Increase to see "Loading...". */
const CATALOG_FILTER_DELAY_MS = 1500

const searchExecutionMode = ref('latency') // 'latency' or 'no-latency'

const runCatalogSearch = async (query) => {
  if (query && lastCatalogSearchQuery.value === query) {
    isCatalogResultsVisible.value = true
    return
  }

  if (lastCatalogSearchQuery.value === '') isCatalogResultsVisible.value = true

  isCatalogLoading.value = true

  if (searchExecutionMode.value === 'latency') {
    await runCatalogSearchWithLatency(query)
  } else {
    await runCatalogSearchWithoutLatency(query)
  }
}

const runCatalogSearchWithoutLatency = async (query) => {
  try {
    const q = String(query).trim().toLowerCase()
    lastCatalogSearchQuery.value = String(query).trim()
    if (!q) {
      filteredCatalogResults.value = []
      return
    }
    filteredCatalogResults.value = CATALOG_ITEMS.filter((item) =>
      item.text.toLowerCase().includes(q)
    )
    isCatalogResultsVisible.value = true
  } finally {
    isCatalogLoading.value = false
  }
}

const runCatalogSearchWithLatency = async (query) => {
  try {
    await new Promise((resolve) => setTimeout(resolve, CATALOG_FILTER_DELAY_MS))
    const q = String(query).trim().toLowerCase()
    lastCatalogSearchQuery.value = String(query).trim()
    if (!q) {
      filteredCatalogResults.value = []
      return
    }
    filteredCatalogResults.value = CATALOG_ITEMS.filter((item) =>
      item.text.toLowerCase().includes(q)
    )
    isCatalogResultsVisible.value = true
  } finally {
    isCatalogLoading.value = false
  }
}

const handleCatalogSearchChanged = (v) => {
  catalogSearchInputValue.value = String(v ?? '')

  if (!String(v ?? '').trim()) {
    filteredCatalogResults.value = []
    lastCatalogSearchQuery.value = ''
    isCatalogResultsVisible.value = false
  }
}
const handleCatalogSearchCleared = () => {
  catalogSearchInputValue.value = ''
  lastCatalogSearchQuery.value = ''
  filteredCatalogResults.value = []
  isCatalogResultsVisible.value = false
  lastCatalogInteractionSource.value = ''
  catalogInteractionSourceCounters.value = { debounce: 0, submit: 0 }
}
const shouldCloseOnInputBlur = ref(false)
const handleCatalogSearchBlurred = () => {
  if (!shouldCloseOnInputBlur.value) return

  isCatalogLoading.value = false
  isCatalogResultsVisible.value = false
}
const handleCatalogSearchFocused = () => {
  if (lastCatalogSearchQuery.value !== '') {
    runCatalogSearch(lastCatalogSearchQuery.value)
  }
}
const handleCatalogResultSelect = (item) => {
  catalogSearchInputValue.value = ''
  lastCatalogSearchQuery.value = ''
  isCatalogResultsVisible.value = false
  filteredCatalogResults.value = []
}
const closeCatalogResults = () => {
  isCatalogResultsVisible.value = false
}
</script>

Behaviour (summary)

interaction-triggerWhile typingEnter / button
submit (default)Does not call interactionFunction.Calls interactionFunction (after entered on that path). Enter requires hasTabIndexEnter.
debounceIf interactionDebounceWait > 0, calls after pause.Cancels pending debounce, emits entered, runs interactionFunction immediately.

Search button: shown for submit, or debounce with interactionDebounceWait <= 0. Hidden for debounce + wait > 0.

Trim: interactionFunction receives the value after hasTrim rules; empty / whitespace-only skips the call; clearing the field can emit interaction-cancel with reason clear-value.

interaction-cancel: emitted for explicit timer clears (submit, clear-value, unmount, reconfigure). Rapid retyping before wait only resets the timer inside magic-debounceno interaction-cancel in that case.

License

MIT