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:
parent
3ea2ba18f2
commit
442111c1ad
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue