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 \qmltype CurrencyAmountInput
\inherits TextField \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. 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. It accepts both the native decimal separator and optionally a period (`.`) for locales that don't use this.
\inqmlmodule shared.controls 1.0 \inqmlmodule shared.controls 1.0
@ -91,11 +91,11 @@ TextField {
background: Rectangle { background: Rectangle {
radius: Style.current.radius radius: Style.current.radius
color: Theme.palette.statusAppNavBar.backgroundColor color: Theme.palette.statusAppNavBar.backgroundColor
border.width: root.cursorVisible || root.hovered || !root.valid ? 1 : 0 border.width: 1
border.color: { border.color: {
if (!root.valid) if (!root.valid && (root.focus || root.cursorVisible))
return Theme.palette.dangerColor1 return Theme.palette.dangerColor1
if (root.cursorVisible) if (root.cursorVisible || root.focus)
return Theme.palette.primaryColor1 return Theme.palette.primaryColor1
if (root.hovered) if (root.hovered)
return Theme.palette.primaryColor2 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 SettingsRadioButton 1.0 SettingsRadioButton.qml
ShapeRectangle 1.0 ShapeRectangle.qml ShapeRectangle 1.0 ShapeRectangle.qml
SocialLinkPreview 1.0 SocialLinkPreview.qml SocialLinkPreview 1.0 SocialLinkPreview.qml
StatusButtonRow 1.0 StatusButtonRow.qml
StatusSyncCodeInput 1.0 StatusSyncCodeInput.qml StatusSyncCodeInput 1.0 StatusSyncCodeInput.qml
StatusSyncCodeScan 1.0 StatusSyncCodeScan.qml StatusSyncCodeScan 1.0 StatusSyncCodeScan.qml
StatusSyncingInstructions 1.0 StatusSyncingInstructions.qml StatusSyncingInstructions 1.0 StatusSyncingInstructions.qml