From 76d769decfba09ec4e45480c675de0c84799389b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Thu, 23 May 2024 10:08:18 +0200 Subject: [PATCH] feat: [UI - Swap] Create row radiobutton component with custom field - Create row radiobutton component like the one defined in design - It shall contain custom set of buttons - It shall contain a Custom button that will be converted to an input field - Add the new component into a new storybook page - Create necessary qml tests to cover the component logic Fixes #14784 --- storybook/pages/StatusButtonRowPage.qml | 83 +++++++++ .../qmlTests/tests/tst_StatusButtonRow.qml | 164 ++++++++++++++++++ .../shared/controls/CurrencyAmountInput.qml | 8 +- .../shared/controls/StatusButtonRow.qml | 99 +++++++++++ ui/imports/shared/controls/qmldir | 1 + 5 files changed, 351 insertions(+), 4 deletions(-) create mode 100644 storybook/pages/StatusButtonRowPage.qml create mode 100644 storybook/qmlTests/tests/tst_StatusButtonRow.qml create mode 100644 ui/imports/shared/controls/StatusButtonRow.qml diff --git a/storybook/pages/StatusButtonRowPage.qml b/storybook/pages/StatusButtonRowPage.qml new file mode 100644 index 00000000000..36d7675ddef --- /dev/null +++ b/storybook/pages/StatusButtonRowPage.qml @@ -0,0 +1,83 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import shared.controls 1.0 +import utils 1.0 + +import Storybook 1.0 + +SplitView { + orientation: Qt.Horizontal + + Logs { id: logs } + + Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + background: Rectangle { + color: Theme.palette.baseColor4 + } + + StatusButtonRow { + id: buttonRow + symbolValue: ctrlCustomSymbol.text + anchors.centerIn: parent + //currentValue: 1.42 + } + } + + LogsAndControlsPanel { + SplitView.fillHeight: true + SplitView.preferredWidth: 300 + + logsView.logText: logs.logText + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.fillWidth: true + Label { text: "Custom symbol:" } + TextField { + Layout.fillWidth: true + id: ctrlCustomSymbol + text: " %" + } + } + Button { + text: "Reset to default" + onClicked: buttonRow.reset() + } + Label { + Layout.fillWidth: true + text: "Model: [%1]".arg(buttonRow.model) + } + Label { + Layout.fillWidth: true + text: "Default value: %1".arg(buttonRow.defaultValue) + } + Label { + Layout.fillWidth: true + font.weight: Font.Medium + text: "Current value: %1".arg(buttonRow.currentValue) + } + Label { + Layout.fillWidth: true + font.weight: Font.Medium + text: "Valid: %1".arg(buttonRow.valid ? "true" : "false") + } + + Item { Layout.fillHeight: true } + } + } +} + +// category: Controls + +// https://www.figma.com/design/TS0eQX9dAZXqZtELiwKIoK/Swap---Milestone-1?node-id=3409-257346&t=ENK93cK7GyTqEV8S-0 +// https://www.figma.com/design/TS0eQX9dAZXqZtELiwKIoK/Swap---Milestone-1?node-id=3410-262441&t=ENK93cK7GyTqEV8S-0 diff --git a/storybook/qmlTests/tests/tst_StatusButtonRow.qml b/storybook/qmlTests/tests/tst_StatusButtonRow.qml new file mode 100644 index 00000000000..42525953865 --- /dev/null +++ b/storybook/qmlTests/tests/tst_StatusButtonRow.qml @@ -0,0 +1,164 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import StatusQ.Controls 0.1 + +import shared.controls 1.0 + +Item { + id: root + width: 600 + height: 400 + + Component { + id: componentUnderTest + StatusButtonRow { + anchors.centerIn: parent + } + } + + property StatusButtonRow controlUnderTest: null + + TestCase { + name: "StatusButtonRow" + when: windowShown + + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + } + + function test_basicGeometry() { + verify(!!controlUnderTest) + verify(controlUnderTest.width > 0) + verify(controlUnderTest.height > 0) + } + + function test_defaultValueIsCurrentAndValid() { + verify(!!controlUnderTest) + verify(controlUnderTest.currentValue === controlUnderTest.defaultValue) + verify(controlUnderTest.valid) + } + + function test_selectPresetValues() { + verify(!!controlUnderTest) + const buttonsRepeater = findChild(controlUnderTest, "buttonsRepeater") + verify(!!buttonsRepeater) + for (let i = 0; i < buttonsRepeater.count; i++) { + const button = buttonsRepeater.itemAt(i) + verify(!!button) + mouseClick(button) + tryCompare(button, "checked", true) + tryCompare(button, "type", StatusBaseButton.Type.Primary) + tryCompare(controlUnderTest, "currentValue", controlUnderTest.model[i]) + verify(controlUnderTest.valid) + } + } + + function test_setAndTypeCustomValue() { + verify(!!controlUnderTest) + const customButton = findChild(controlUnderTest, "customButton") + verify(!!customButton) + mouseClick(customButton) + const customInput = findChild(controlUnderTest, "customInput") + verify(!!customInput) + tryCompare(customInput, "cursorVisible", true) + + // input "1.42" + keyClick(Qt.Key_1) + keyClick(Qt.Key_Period) + keyClick(Qt.Key_4) + keyClick(Qt.Key_2) + + tryCompare(controlUnderTest, "currentValue", 1.42) + verify(controlUnderTest.valid) + + // delete contents (4x) + keyClick(Qt.Key_Backspace) + keyClick(Qt.Key_Backspace) + keyClick(Qt.Key_Backspace) + keyClick(Qt.Key_Backspace) + + tryCompare(customInput, "text", "") + tryCompare(customInput, "valid", false) + tryCompare(controlUnderTest, "valid", false) + + // click again the first button + const buttonsRepeater = findChild(controlUnderTest, "buttonsRepeater") + verify(!!buttonsRepeater) + const firstButton = buttonsRepeater.itemAt(0) + verify(!!firstButton) + mouseClick(firstButton) + tryCompare(controlUnderTest, "currentValue", firstButton.value) + verify(controlUnderTest.valid) + } + + function test_setCustomInitialValue() { + controlUnderTest.destroy() + controlUnderTest = createTemporaryObject(componentUnderTest, root, {currentValue: 1.42}) + + verify(!!controlUnderTest) + verify(controlUnderTest.valid) + const customInput = findChild(controlUnderTest, "customInput") + verify(!!customInput) + tryCompare(customInput, "cursorVisible", true) + tryCompare(customInput, "value", 1.42) + } + + function test_resetDefaults() { + verify(!!controlUnderTest) + const buttonsRepeater = findChild(controlUnderTest, "buttonsRepeater") + verify(!!buttonsRepeater) + const firstButton = buttonsRepeater.itemAt(0) + verify(!!firstButton) + mouseClick(firstButton) + tryCompare(controlUnderTest, "currentValue", firstButton.value) + tryCompare(controlUnderTest, "currentValue", controlUnderTest.model[0]) + + controlUnderTest.reset() + tryCompare(controlUnderTest, "currentValue", controlUnderTest.defaultValue) + verify(controlUnderTest.valid) + } + + function test_customSymbolValue() { + const customSymbol = "+++" + verify(!!controlUnderTest) + controlUnderTest.symbolValue = customSymbol + + const buttonsRepeater = findChild(controlUnderTest, "buttonsRepeater") + verify(!!buttonsRepeater) + for (let i = 0; i < buttonsRepeater.count; i++) { + const button = buttonsRepeater.itemAt(i) + verify(!!button) + verify(button.text.endsWith(customSymbol)) + } + + const customButton = findChild(controlUnderTest, "customButton") + verify(!!customButton) + mouseClick(customButton) + const customInput = findChild(controlUnderTest, "customInput") + verify(!!customInput) + verify(customInput.currencySymbol === customSymbol) + } + + function test_customModel() { + controlUnderTest.destroy() + controlUnderTest = createTemporaryObject(componentUnderTest, root, + {model: [.1, .2, .3, .4, .5]}) + + verify(!!controlUnderTest) + verify(controlUnderTest.currentValue === controlUnderTest.defaultValue) + verify(controlUnderTest.valid) + + const buttonsRepeater = findChild(controlUnderTest, "buttonsRepeater") + verify(!!buttonsRepeater) + verify(buttonsRepeater.count === controlUnderTest.model.length) + const firstButton = buttonsRepeater.itemAt(0) + verify(!!firstButton) + mouseClick(firstButton) + tryCompare(firstButton, "checked", true) + tryCompare(controlUnderTest, "currentValue", firstButton.value) + tryCompare(controlUnderTest, "currentValue", controlUnderTest.model[0]) + verify(controlUnderTest.valid) + } + } +} diff --git a/ui/imports/shared/controls/CurrencyAmountInput.qml b/ui/imports/shared/controls/CurrencyAmountInput.qml index 027e40363c0..9c266f5203f 100644 --- a/ui/imports/shared/controls/CurrencyAmountInput.qml +++ b/ui/imports/shared/controls/CurrencyAmountInput.qml @@ -12,7 +12,7 @@ import utils 1.0 /*! \qmltype CurrencyAmountInput \inherits TextField - \brief Provides a text input field that accepts a numeric value, with optional currency symbol ("USD"). + \brief Provides a text input field that accepts a numeric value, with optional (currency) symbol (defaults to "USD"). Utilizes a builtin DoubleValidator to validate the user's input. It accepts both the native decimal separator and optionally a period (`.`) for locales that don't use this. \inqmlmodule shared.controls 1.0 @@ -91,11 +91,11 @@ TextField { background: Rectangle { radius: Style.current.radius color: Theme.palette.statusAppNavBar.backgroundColor - border.width: root.cursorVisible || root.hovered || !root.valid ? 1 : 0 + border.width: 1 border.color: { - if (!root.valid) + if (!root.valid && (root.focus || root.cursorVisible)) return Theme.palette.dangerColor1 - if (root.cursorVisible) + if (root.cursorVisible || root.focus) return Theme.palette.primaryColor1 if (root.hovered) return Theme.palette.primaryColor2 diff --git a/ui/imports/shared/controls/StatusButtonRow.qml b/ui/imports/shared/controls/StatusButtonRow.qml new file mode 100644 index 00000000000..f590c3ef387 --- /dev/null +++ b/ui/imports/shared/controls/StatusButtonRow.qml @@ -0,0 +1,99 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 + +Control { + id: root + + property var model: [0.1, 0.5, 1] + property double defaultValue: 0.5 + property string symbolValue: " %" + + property alias currentValue: d.currentValue + + readonly property bool valid: d.currentValue && (d.customInputFocused ? customLoader.item.valid : true) + + function reset() { + customLoader.sourceComponent = customButtonComponent + d.currentValue = root.defaultValue + } + + Component.onCompleted: { + if (currentValue && !root.model.includes(currentValue)) + d.activateCustomInput() + } + + QtObject { + id: d + + property double currentValue: root.defaultValue + + readonly property bool customInputFocused: customLoader.sourceComponent === customInputComponent && customLoader.item.focus + + function activateCustomInput() { + customLoader.sourceComponent = customInputComponent + customLoader.item.forceActiveFocus() + } + } + + background: null + contentItem: RowLayout { + spacing: Style.current.halfPadding + + Repeater { + objectName: "buttonsRepeater" + model: root.model + delegate: StatusButton { + readonly property double value: modelData + Layout.minimumWidth: 100 + Layout.fillWidth: true + type: checked ? StatusBaseButton.Type.Primary : StatusBaseButton.Type.Normal + checkable: true + checked: value === d.currentValue && !d.customInputFocused + text: "%L1%2".arg(modelData).arg(root.symbolValue) + onClicked: d.currentValue = value + } + } + Loader { + id: customLoader + objectName: "customLoader" + Layout.minimumWidth: 130 + Layout.fillWidth: true + sourceComponent: customButtonComponent + } + } + + Component { + id: customButtonComponent + StatusButton { + objectName: "customButton" + text: qsTr("Custom") + onClicked: d.activateCustomInput() + } + } + Component { + id: customInputComponent + CurrencyAmountInput { + objectName: "customInput" + minValue: 0.01 + currencySymbol: root.symbolValue + focus: value === d.currentValue + onValueChanged: d.currentValue = value + onFocusChanged: { + if (focus && valid) + d.currentValue = value + else if (!valid) + clear() + } + Component.onCompleted: { + if (d.currentValue && d.currentValue !== root.defaultValue && !root.model.includes(d.currentValue)) + value = d.currentValue + } + } + } +} diff --git a/ui/imports/shared/controls/qmldir b/ui/imports/shared/controls/qmldir index 1260a6eb676..6d653d06254 100644 --- a/ui/imports/shared/controls/qmldir +++ b/ui/imports/shared/controls/qmldir @@ -32,6 +32,7 @@ SendToContractWarning 1.0 SendToContractWarning.qml SettingsRadioButton 1.0 SettingsRadioButton.qml ShapeRectangle 1.0 ShapeRectangle.qml SocialLinkPreview 1.0 SocialLinkPreview.qml +StatusButtonRow 1.0 StatusButtonRow.qml StatusSyncCodeInput 1.0 StatusSyncCodeInput.qml StatusSyncCodeScan 1.0 StatusSyncCodeScan.qml StatusSyncingInstructions 1.0 StatusSyncingInstructions.qml