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
This commit is contained in:
Lukáš Tinkl 2024-05-23 10:08:18 +02:00 committed by Lukáš Tinkl
parent 3ea2ba18f2
commit 442111c1ad
5 changed files with 351 additions and 4 deletions

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -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