page ToggleCountcomponent { seo { title = "ToggleCount component" description = "Theme-aware, responsive toggle count component." } view {
W WRNexusJS
advanced-forms component

ToggleCount

Theme-aware, responsive toggle count component.

@wrnexus/ui 0.5.023 props0 slots2 events

Usage

Components are auto-discovered. Use the component directly in any .wrn view:

<ToggleCount
  size="default"
  color="primary"
  variant="segmented"
  name="billing-cycle"
  value="monthly"
/>

Legacy mount name: data-component="ToggleCount".

Props and attributes

PropTypeRequirementDefault
sizestringOptional"default"
colorstringOptional"primary"
variantstringOptional"segmented"
classstringOptional""
namestringOptional"billing-cycle"
valuestringOptional"monthly"
firstValuestringOptional"monthly"
firstLabelstringOptional"Monthly"
secondValuestringOptional"annual"
secondLabelstringOptional"Annual"
ariaLabelstringOptional"Billing frequency"
itemsstringOptional[]
currencystringOptional"$"
suffixstringOptional""
firstValueKeystringOptional"monthly"
secondValueKeystringOptional"annual"
emptyValuestringOptional"—"
alignstringOptional"end"
fullWidthbooleanOptionaltrue
disabledbooleanOptionalfalse
animatebooleanOptionaltrue
animationDurationnumberOptional450
animationStepsnumberOptional18

Slots

This component does not declare a slot.

Events

  • change
  • toggle

Source contract

Installed source: components/ToggleCount.wrn

component ToggleCount {
  props {
    size = "default"
    color = "primary"
    variant = "segmented"
    class = ""

    name = "billing-cycle"
    value = "monthly"
    firstValue = "monthly"
    firstLabel = "Monthly"
    secondValue = "annual"
    secondLabel = "Annual"
    ariaLabel = "Billing frequency"

    items = []
    currency = "$"
    suffix = ""
    firstValueKey = "monthly"
    secondValueKey = "annual"
    emptyValue = "—"

    align = "end"
    fullWidth = true
    disabled = false
    animate = true
    animationDuration = 450
    animationSteps = 18

    @event change = function
    @event toggle = function
  }

  state selectedValue = value

  functions {
    function isFirstSelected() {
      return selectedValue === firstValue
    }

    function isSecondSelected() {
      return selectedValue === secondValue
    }

    function displayValue(item) {
      if (isSecondSelected()) {
        return item[secondValueKey] !== undefined
          ? item[secondValueKey]
          : emptyValue
      }

      return item[firstValueKey] !== undefined
        ? item[firstValueKey]
        : emptyValue
    }

    function dispatchToggleEvent(sourceEvent, previousValue, root, customEvent) {
      root = sourceEvent.currentTarget.closest("[data-wrn-toggle-count]")

      if (!root) {
        return
      }

      customEvent = document.createEvent("CustomEvent")
      customEvent.initCustomEvent("change", true, false, {
        component: "ToggleCount",
        name: name,
        value: selectedValue,
        previousValue: previousValue,
        firstValue: firstValue,
        secondValue: secondValue
      })
      root.dispatchEvent(customEvent)

      customEvent = document.createEvent("CustomEvent")
      customEvent.initCustomEvent("toggle", true, false, {
        component: "ToggleCount",
        name: name,
        value: selectedValue,
        previousValue: previousValue,
        firstValue: firstValue,
        secondValue: secondValue
      })
      root.dispatchEvent(customEvent)
    }

    function selectValue(sourceEvent, nextValue, previousValue) {
      if (disabled || selectedValue === nextValue) {
        return
      }

      previousValue = selectedValue
      selectedValue = nextValue

      if (
        animate &&
        !window.matchMedia("(prefers-reduced-motion: reduce)").matches
      ) {
        animateDisplayedValues(sourceEvent, previousValue)
      }

      dispatchToggleEvent(sourceEvent, previousValue)
    }

    function animateDisplayedValues(sourceEvent, previousValue, root, nodes) {
      root = sourceEvent.currentTarget.closest("[data-wrn-toggle-count]")

      if (!root) {
        return
      }

      nodes = root.querySelectorAll("[data-toggle-count-value]")
      animateValueAt(nodes, 0, previousValue)
    }

    function animateValueAt(nodes, index, previousValue, node, fromValue, toValue) {
      if (index >= nodes.length) {
        return
      }

      node = nodes[index]
      fromValue = Number(
        previousValue === secondValue
          ? node.getAttribute("data-second-value")
          : node.getAttribute("data-first-value")
      )
      toValue = Number(
        selectedValue === secondValue
          ? node.getAttribute("data-second-value")
          : node.getAttribute("data-first-value")
      )

      if (!Number.isNaN(fromValue) && !Number.isNaN(toValue)) {
        animateValueFrame(node, fromValue, toValue, 0)
      }

      animateValueAt(nodes, index + 1, previousValue)
    }

    function animateValueFrame(node, fromValue, toValue, frame, totalFrames, nextValue) {
      totalFrames = Math.max(1, Number(animationSteps) || 1)

      if (frame >= totalFrames) {
        node.replaceChildren(String(toValue))
        return
      }

      nextValue = Math.round(
        fromValue +
        (toValue - fromValue) * (frame / totalFrames)
      )
      node.replaceChildren(String(nextValue))

      setTimeout(
        animateValueFrame,
        Math.max(1, Number(animationDuration) / totalFrames),
        node,
        fromValue,
        toValue,
        frame + 1
      )
    }

    function toggleValue(sourceEvent) {
      selectValue(
        sourceEvent,
        isFirstSelected() ? secondValue : firstValue
      )
    }
  }

  view {
        <section
      {...attrs}
      data-wrn-toggle-count
      data-variant="{variant}"
      data-value="{selectedValue}"
      data-disabled="{disabled ? 'true' : 'false'}"
      class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--toggle-count wire-next--toggle-count-{variant} {fullWidth ? 'wire-next--toggle-count-full' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
    >
        <input type="hidden" name="{name}" value="{selectedValue}" />
        <div class="wire-next__toggle-count-control wire-next__toggle-count-control--{align}">
        {#if variant === "switch"}
          <span data-selected="{isFirstSelected() ? 'true' : 'false'}">{firstLabel}</span>
        <button
            type="button"
            role="switch"
            aria-label="{ariaLabel}"
            aria-checked="{isSecondSelected() ? 'true' : 'false'}"
            disabled="{disabled}"
            @click="toggleValue(event)"
            class="wire-next__toggle-count-switch"
          >
        <span aria-hidden="true">
        </span>
        </button>
        <span data-selected="{isSecondSelected() ? 'true' : 'false'}">{secondLabel}</span>
        {:else}
          <div class="wire-next__toggle-count-segmented" role="group" aria-label="{ariaLabel}">
        <button
              type="button"
              aria-pressed="{isFirstSelected() ? 'true' : 'false'}"
              disabled="{disabled}"
              @click="selectValue(event, firstValue)"
            >{firstLabel}</button>
        <button
              type="button"
              aria-pressed="{isSecondSelected() ? 'true' : 'false'}"
              disabled="{disabled}"
              @click="selectValue(event, secondValue)"
            >{secondLabel}</button>
        </div>
        {/if}
      </div>

      {#if items.length > 0}
        <div class="wire-next__toggle-count-items">
          {#each items as item}
            <article>
        <span>{item.label || item.name}</span>
        <strong>
                {#if currency}<small>{currency}</small>{/if}
                <span
                  data-toggle-count-value
                  data-first-value="{item[firstValueKey]}"
                  data-second-value="{item[secondValueKey]}"
                >{displayValue(item)}</span>
                {#if suffix}<small>{suffix}</small>{/if}
              </strong>
              {#if item.description}<small>{item.description}</small>{/if}
            </article>
          {/each}
        </div>
      {/if}
    </section>
    }
}
} }