diff options
5 files changed, 114 insertions, 4 deletions
diff --git a/src/quickcontrols/doc/src/qtquickcontrols-material.qdoc b/src/quickcontrols/doc/src/qtquickcontrols-material.qdoc index 0bfb2bed80..7dd21c29ac 100644 --- a/src/quickcontrols/doc/src/qtquickcontrols-material.qdoc +++ b/src/quickcontrols/doc/src/qtquickcontrols-material.qdoc @@ -221,6 +221,27 @@ Note that the heights shown above may vary based on differences in fonts across platforms. + \section2 Control-Specific Notes + + \target material-control-specific-notes-textarea + \section3 TextArea + + TextArea supports two + \l {material-containerStyle-attached-prop}{containerStyles}: \c Filled and + \c Outlined. Outlined TextAreas have floating placeholder text that sits at + the top of the control. This requires the placeholder text to go outside + the bounds of the control, which can cause it to be + clipped when the TextArea or the Flickable it's a child of sets + \l {Item::}{clip} to \c true. To avoid this, \l {Control::}{topInset} is + set to an appropriate value in these cases. + + \section3 TextField + + The same \l {material-control-specific-notes-textarea}{issue with clipping} + explained above for TextArea can also occur with \l TextField. To avoid + this, \l {Control::}{topInset} is set to an appropriate value when the + TextField sets clip to \c true. + \section1 Attached Property Documentation \styleproperty {Material.accent} {color} {material-accent-attached-prop} diff --git a/src/quickcontrols/material/TextArea.qml b/src/quickcontrols/material/TextArea.qml index 58eef10231..4a975295a5 100644 --- a/src/quickcontrols/material/TextArea.qml +++ b/src/quickcontrols/material/TextArea.qml @@ -16,6 +16,10 @@ T.TextArea { implicitHeight: Math.max(contentHeight + topPadding + bottomPadding, implicitBackgroundHeight + topInset + bottomInset) + // If we're clipped, or we're in a Flickable that's clipped, set our topInset + // to half the height of the placeholder text to avoid it being clipped. + topInset: clip || (parent?.parent as Flickable && parent?.parent.clip) ? placeholder.largestHeight / 2 : 0 + leftPadding: Material.textFieldHorizontalPadding rightPadding: Material.textFieldHorizontalPadding // Need to account for the placeholder text when it's sitting on top. @@ -23,7 +27,8 @@ T.TextArea { ? Material.textFieldVerticalPadding + placeholder.largestHeight // When the condition above is not met, the text should always sit in the middle // of a default-height TextArea, which is just near the top for a higher-than-default one. - : (implicitBackgroundHeight - placeholder.largestHeight) / 2 + // Account for any topInset as well, otherwise the text will be too close to the background. + : ((implicitBackgroundHeight - placeholder.largestHeight) / 2) + topInset bottomPadding: Material.textFieldVerticalPadding color: enabled ? Material.foreground : Material.hintTextColor diff --git a/src/quickcontrols/material/TextField.qml b/src/quickcontrols/material/TextField.qml index 97174de188..d29b247ce1 100644 --- a/src/quickcontrols/material/TextField.qml +++ b/src/quickcontrols/material/TextField.qml @@ -15,6 +15,9 @@ T.TextField { implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + // If we're clipped, set topInset to half the height of the placeholder text to avoid it being clipped. + topInset: clip ? placeholder.largestHeight / 2 : 0 + leftPadding: Material.textFieldHorizontalPadding rightPadding: Material.textFieldHorizontalPadding // Need to account for the placeholder text when it's sitting on top. @@ -22,7 +25,9 @@ T.TextField { ? placeholderText.length > 0 && (activeFocus || length > 0) ? Material.textFieldVerticalPadding + placeholder.largestHeight : Material.textFieldVerticalPadding - : Material.textFieldVerticalPadding + // Account for any topInset (used to avoid floating placeholder text being clipped), + // otherwise the text will be too close to the background. + : Material.textFieldVerticalPadding + topInset bottomPadding: Material.textFieldVerticalPadding color: enabled ? Material.foreground : Material.hintTextColor diff --git a/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp b/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp index 7ec3386457..8533802a5a 100644 --- a/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp +++ b/src/quickcontrols/material/impl/qquickmaterialplaceholdertext.cpp @@ -10,6 +10,7 @@ #include <QtQml/qqmlinfo.h> #include <QtQuickTemplates2/private/qquicktheme_p.h> #include <QtQuickTemplates2/private/qquicktextarea_p.h> +#include <QtQuickTemplates2/private/qquicktextfield_p.h> QT_BEGIN_NAMESPACE @@ -106,6 +107,17 @@ void QQuickMaterialPlaceholderText::updateY() setY(shouldFloat() ? floatingTargetY() : normalTargetY()); } +qreal controlTopInset(QQuickItem *textControl) +{ + if (const auto textArea = qobject_cast<QQuickTextArea *>(textControl)) + return textArea->topInset(); + + if (const auto textField = qobject_cast<QQuickTextField *>(textControl)) + return textField->topInset(); + + return 0; +} + qreal QQuickMaterialPlaceholderText::normalTargetY() const { auto *textArea = qobject_cast<QQuickTextArea *>(textControl()); @@ -115,7 +127,10 @@ qreal QQuickMaterialPlaceholderText::normalTargetY() const // (one-line) if its explicit height is greater than or equal to its // implicit height - i.e. if it has room for it. If it doesn't have // room, just do what TextField does. - return (m_controlImplicitBackgroundHeight - m_largestHeight) / 2.0; + // We should also account for any topInset the user might have specified, + // which is useful to ensure that the text doesn't get clipped. + return ((m_controlImplicitBackgroundHeight - m_largestHeight) / 2.0) + + controlTopInset(textControl()); } // When the placeholder text shouldn't float, it should sit in the middle of the TextField. @@ -131,7 +146,7 @@ qreal QQuickMaterialPlaceholderText::floatingTargetY() const // Outlined text fields have the placeaholder vertically centered // along the outline at the top. - return -m_largestHeight / 2; + return (-m_largestHeight / 2) + controlTopInset(textControl()); } /*! diff --git a/tests/auto/quickcontrols/qquickmaterialstyle/data/tst_material.qml b/tests/auto/quickcontrols/qquickmaterialstyle/data/tst_material.qml index 8e4189924f..f77c0c5fc8 100644 --- a/tests/auto/quickcontrols/qquickmaterialstyle/data/tst_material.qml +++ b/tests/auto/quickcontrols/qquickmaterialstyle/data/tst_material.qml @@ -1082,4 +1082,68 @@ TestCase { verify(textContainer as MaterialImpl.MaterialTextContainer) compare(textContainer.focusAnimationProgress, 0) } + + function test_outlinedTextAreaInFlickablePlaceholderTextClipping() { + let flickable = createTemporaryObject(flickableTextAreaComponent, testCase) + verify(flickable) + + let textArea = flickable.TextArea.flickable + verify(textArea) + let placeholderTextItem = flickable.children[2] + verify(placeholderTextItem as MaterialImpl.FloatingPlaceholderText) + compare(textArea.Material.containerStyle, Material.Outlined) + // The Flickable doesn't clip at the moment so topInset should be 0. + compare(textArea.topInset, 0) + compare(textArea.topPadding, (textArea.implicitBackgroundHeight - placeholderTextItem.largestHeight) / 2) + + // topInset should now be half the placeholder text's height, + // and topPadding adjusted accordingly. + flickable.clip = true + compare(textArea.topInset, placeholderTextItem.largestHeight / 2) + compare(textArea.topPadding, ((textArea.implicitBackgroundHeight - placeholderTextItem.largestHeight) / 2) + textArea.topInset) + + // When the text is cleared, the placeholder text shouldn't float, but it should still be accounted for + // to avoid it causing jumps in layout sizes, for example. + const initialText = textArea.text + textArea.text = "" + compare(textArea.topPadding, ((textArea.implicitBackgroundHeight - placeholderTextItem.largestHeight) / 2) + textArea.topInset) + + flickable.clip = false + compare(textArea.topInset, 0) + compare(textArea.topPadding, (textArea.implicitBackgroundHeight - placeholderTextItem.largestHeight) / 2) + } + + function test_outlinedTextAreaPlaceholderTextClipping() { + let textArea = createTemporaryObject(textAreaComponent, testCase, { + placeholderText: "Type something", + text: "Text" + }) + verify(textArea) + let placeholderTextItem = textArea.children[0] + verify(placeholderTextItem as MaterialImpl.FloatingPlaceholderText) + compare(textArea.topInset, 0) + compare(textArea.topPadding, (textArea.implicitBackgroundHeight - placeholderTextItem.largestHeight) / 2) + + // topInset should now be half the placeholder text's height, and topPadding adjusted accordingly. + textArea.clip = true + compare(textArea.topInset, placeholderTextItem.largestHeight / 2) + compare(textArea.topPadding, ((textArea.implicitBackgroundHeight - placeholderTextItem.largestHeight) / 2) + textArea.topInset) + } + + function test_outlinedTextFieldPlaceholderTextClipping() { + let textField = createTemporaryObject(textFieldComponent, testCase, { + placeholderText: "Type something", + text: "Text" + }) + verify(textField) + let placeholderTextItem = textField.children[0] + verify(placeholderTextItem as MaterialImpl.FloatingPlaceholderText) + compare(textField.topInset, 0) + compare(textField.topPadding, textField.Material.textFieldVerticalPadding) + + // topInset should now be half the placeholder text's height, and topPadding adjusted accordingly. + textField.clip = true + compare(textField.topInset, placeholderTextItem.largestHeight / 2) + compare(textField.topPadding, textField.Material.textFieldVerticalPadding + textField.topInset) + } } |