homescreens/folio: Add keyboard navigation to app drawer and fix on

search

This commit is a subset of
https://invent.kde.org/plasma/plasma-mobile/-/merge_requests/694 to add
keyboard navigation to the app drawer.

Pressing the arrow keys while in the app drawer will allow you to
navigate between the apps and the search bar, and going up will exit the
app drawer. Escape/Back is also supported for unfocusing the search bar
and exiting the view.

This also fixes an issue in the keyboard navigation on the search screen
where it wouldn't close when there are no search results.
This commit is contained in:
Devin Lin 2025-07-10 12:54:01 -04:00
parent 04bcbd9f8d
commit 0bcab0ae3a
7 changed files with 156 additions and 10 deletions

View file

@ -72,7 +72,21 @@ MouseArea {
font.weight: Font.Bold
KeyNavigation.down: listView
// Keyboard navigation
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Down) {
if (listView.count === 0) {
// Close if listview has no elements
root.requestedClose();
} else {
// Focus on listview if there are elements
listView.forceActiveFocus();
listView.currentIndex = 0;
}
event.accepted = true;
}
}
}
QQC2.ScrollView {

View file

@ -28,11 +28,21 @@ Item {
required property int headerHeight
required property var headerItem
// height from top of screen that the drawer starts
// Height from top of screen that the drawer starts
readonly property real drawerTopMargin: height - topPadding - bottomPadding
property alias flickable: appDrawerGrid
// Keyboard navigation
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape || event.key === Qt.Key_Back) {
// Close drawer if "back" action
folio.HomeScreenState.closeAppDrawer();
event.accepted = true;
}
}
// App drawer container
Item {
anchors.fill: parent
@ -41,7 +51,7 @@ Item {
anchors.rightMargin: root.rightPadding
anchors.bottomMargin: root.bottomPadding
// drawer header
// Drawer header
MobileShell.BaseItem {
id: drawerHeader
z: 1
@ -52,8 +62,23 @@ Item {
anchors.right: parent.right
contentItem: root.headerItem
// Keyboard navigation for header (search bar)
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Down || event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) {
// Go from search bar to app grid
appDrawerGrid.forceActiveFocus();
appDrawerGrid.currentIndex = 0;
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
// Go to homescreen pages
folio.HomeScreenState.closeAppDrawer();
event.accepted = true;
}
}
}
// App list
AppDrawerGrid {
id: appDrawerGrid
folio: root.folio
@ -64,9 +89,22 @@ Item {
anchors.bottom: parent.bottom
opacity: 0 // we display with the opacity gradient below
headerHeight: root.headerHeight
// Keyboard navigation
topEdgeCallback: () => {
drawerHeader.contentItem.forceActiveFocus();
currentIndex = -1;
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) {
topEdgeCallback();
event.accepted = true;
}
}
}
// opacity gradient at grid edges
// Opacity gradient at grid edges
MobileShell.FlickableOpacityGradient {
anchors.fill: appDrawerGrid
flickable: appDrawerGrid

View file

@ -23,6 +23,10 @@ MobileShell.GridView {
reuseItems: true
layer.enabled: true
keyNavigationEnabled: true
highlightMoveDuration: 0
highlight: null // We supply our own highlight from the delegate
property var homeScreen
property real headerHeight
@ -84,6 +88,13 @@ MobileShell.GridView {
model: folio.ApplicationListSearchModel
// Keyboard focus on app delegate when it is the selected item
onCurrentItemChanged: {
if (currentItem) {
currentItem.keyboardFocus();
}
}
delegate: AppDelegate {
id: appDelegate

View file

@ -18,10 +18,31 @@ Item {
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
function addSearchText(text: string) {
searchField.text += text;
}
function clearSearchText(): void {
searchField.text = '';
}
// Request to not focus on the search bar
signal releaseFocusRequested()
onFocusChanged: {
if (focus) {
searchField.focus = true;
}
}
// Keyboard navigation
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape || event.key === Qt.Key_Back) {
root.releaseFocusRequested();
event.accepted = true;
}
}
RowLayout {
anchors.topMargin: Kirigami.Units.largeSpacing
anchors.leftMargin: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing

View file

@ -437,17 +437,38 @@ Item {
y: (opacity > 0) ? animationY : parent.height
headerHeight: Math.round(Kirigami.Units.gridUnit * 4)
headerItem: AppDrawerHeader { folio: root.folio }
headerItem: AppDrawerHeader {
id: appDrawerHeader
folio: root.folio
// account for panels
onReleaseFocusRequested: appDrawer.forceActiveFocus()
}
// Account for panels
topPadding: root.topMargin
bottomPadding: root.bottomMargin
leftPadding: root.leftMargin
rightPadding: root.rightMargin
// Forward keyboard text to the search bar
Keys.onPressed: (event) => {
if (event.text.trim().length > 0) {
appDrawerHeader.addSearchText(event.text);
appDrawerHeader.forceActiveFocus();
event.accepted = true;
} else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
appDrawerHeader.forceActiveFocus();
event.accepted = true;
}
}
Connections {
target: folio.HomeScreenState
function onAppDrawerOpened() {
appDrawer.forceActiveFocus();
}
function onAppDrawerClosed() {
// reset app drawer position when closed
appDrawer.flickable.contentY = 0;

View file

@ -33,6 +33,10 @@ Folio.DelegateTouchArea {
property real scaleAmount: 1
property bool clickRequested: false
function keyboardFocus() {
delegateWrapper.forceActiveFocus();
}
NumberAnimation on scaleAmount {
id: shrinkAnim
running: false
@ -70,17 +74,36 @@ Folio.DelegateTouchArea {
// trigger handled by press animation
onClicked: clickRequested = true;
layer.enabled: root.shadow
layer.effect: DelegateShadow {}
Item {
FocusScope {
id: delegateWrapper
anchors.fill: parent
// Select keyboard navigation
Keys.onPressed: (event) => {
switch (event.key) {
case Qt.Key_Enter:
case Qt.Key_Return:
case Qt.Key_Space:
root.afterClickAnimation();
event.accepted = true;
break;
default:
break;
}
}
KeyboardHighlight {
anchors.fill: parent
visible: delegateWrapper.activeFocus
}
ColumnLayout {
anchors.fill: parent
spacing: 0
layer.enabled: root.shadow
layer.effect: DelegateShadow {}
// transform is not on delegateWrapper because when it's zoomed in, it apparently
// affects the delegate's x and y position, which messes up the starting drag and drop
// position (for mapFromItem in HomeScreen.qml)

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import org.kde.ksvg as KSvg
import org.kde.kirigami as Kirigami
Rectangle {
id: background
radius: Kirigami.Units.cornerRadius
border.width: 1
border.color: Kirigami.Theme.highlightColor
border.pixelAligned: false
color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.2)
}