From d3ac824912a3e65103777d2cdba6fad0eb157864 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 1 Apr 2026 11:21:01 +0200 Subject: [PATCH] :bug: Fix ICounted error on numeric-input token dropdown keyboard nav (#8803) The options stored in options-ref is a delay (lazy value). In on-token-key-down, it was passed raw to next-focus-index without being dereferenced first, causing count to be called on a JS object that does not implement ICounted. Fix: dereference the delay in on-token-key-down (matching the existing pattern in on-key-down), and make next-focus-index itself also handle delays defensively. Add unit tests covering the delay case. --- .../main/ui/ds/controls/numeric_input.cljs | 6 ++- frontend/test/frontend_tests/runner.cljs | 2 + .../ui/ds_controls_numeric_input_test.cljs | 37 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/ds_controls_numeric_input_test.cljs diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index 63cb9d3bf4..bb9cff8011 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -136,9 +136,10 @@ [options] (some #(when (focusable-option? %) (:id %)) options)) -(defn- next-focus-index +(defn next-focus-index [options focused-id direction] - (let [len (count options) + (let [options (if (delay? options) @options options) + len (count options) start-index (or (d/index-of-pred options #(= focused-id (:id %))) -1) indices (case direction :down (range (inc start-index) (+ len start-index)) @@ -586,6 +587,7 @@ up? (kbd/up-arrow? event) down? (kbd/down-arrow? event) options (mf/ref-val options-ref) + options (if (delay? options) @options options) detach-btn (mf/ref-val token-detach-btn-ref) target (dom/get-target event)] diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 488c5f9cf2..1d189cbafe 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -21,6 +21,7 @@ [frontend-tests.tokens.style-dictionary-test] [frontend-tests.tokens.token-errors-test] [frontend-tests.tokens.workspace-tokens-remap-test] + [frontend-tests.ui.ds-controls-numeric-input-test] [frontend-tests.util-object-test] [frontend-tests.util-range-tree-test] [frontend-tests.util-simple-math-test] @@ -55,6 +56,7 @@ 'frontend-tests.tokens.logic.token-remapping-test 'frontend-tests.tokens.style-dictionary-test 'frontend-tests.tokens.token-errors-test + 'frontend-tests.ui.ds-controls-numeric-input-test 'frontend-tests.util-object-test 'frontend-tests.util-range-tree-test 'frontend-tests.util-simple-math-test diff --git a/frontend/test/frontend_tests/ui/ds_controls_numeric_input_test.cljs b/frontend/test/frontend_tests/ui/ds_controls_numeric_input_test.cljs new file mode 100644 index 0000000000..79d37a2c22 --- /dev/null +++ b/frontend/test/frontend_tests/ui/ds_controls_numeric_input_test.cljs @@ -0,0 +1,37 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns frontend-tests.ui.ds-controls-numeric-input-test + (:require + [app.main.ui.ds.controls.numeric-input :refer [next-focus-index]] + [cljs.test :as t :include-macros true])) + +(def ^:private sample-options + [{:id "a" :type :item :name "Alpha"} + {:id "b" :type :group :name "Group"} + {:id "c" :type :item :name "Charlie"} + {:id "d" :type :separator :name "---"} + {:id "e" :type :item :name "Echo"}]) + +(t/deftest test-next-focus-index + (t/testing "returns index of next focusable item going down" + (t/is (= 2 (next-focus-index sample-options "a" :down)))) + + (t/testing "returns index of next focusable item going up" + (t/is (= 0 (next-focus-index sample-options "c" :up)))) + + (t/testing "wraps around going down" + (t/is (= 0 (next-focus-index sample-options "e" :down)))) + + (t/testing "wraps around going up" + (t/is (= 4 (next-focus-index sample-options "a" :up)))) + + (t/testing "works when options is a delay" + (let [delayed-options (delay sample-options)] + (t/is (= 2 (next-focus-index delayed-options "a" :down))))) + + (t/testing "works with nil focused-id (no current selection)" + (t/is (= 0 (next-focus-index sample-options nil :down)))))