Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 587b4cd4 authored by Josh's avatar Josh
Browse files

Fixed focus issues with left side panel

- fixed issue where user had to tab twice to move focus to next category
- fixed issue where user had to press enter twice to select category.
- created custom surfaces to disable default focus/hover states by
  setting indication=null

Test: Manual, navigate through left side panel using tab and enter only,
ensure that you don't need to press tab twice or enter twice to navigate
or select category.
Flag: com.android.systemui.keyboard_shortcut_helper_rewrite
Fixes: 360298063
Fixes: 358562203

Change-Id: I8fbd2045ebc077235c818d1f7bca4dd0ba9e0c0e
parent c97ebc3e
Loading
Loading
Loading
Loading
+5 −7
Original line number Diff line number Diff line
@@ -731,14 +731,13 @@ private fun CategoryItemTwoPane(
    val interactionSource = remember { MutableInteractionSource() }
    val isFocused by interactionSource.collectIsFocusedAsState()

    Surface(
    SelectableShortcutSurface(
        selected = selected,
        onClick = onClick,
        modifier =
            Modifier.semantics { role = Role.Tab }
                .heightIn(min = 64.dp)
                .fillMaxWidth()
                .focusable(interactionSource = interactionSource)
                .outlineFocusModifier(
                    isFocused = isFocused,
                    focusColor = MaterialTheme.colorScheme.secondary,
@@ -747,6 +746,7 @@ private fun CategoryItemTwoPane(
                ),
        shape = RoundedCornerShape(28.dp),
        color = colors.containerColor(selected).value,
        interactionSource = interactionSource
    ) {
        Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) {
            ShortcutCategoryIcon(
@@ -846,14 +846,12 @@ private fun ShortcutsSearchBar(onQueryChange: (String) -> Unit) {
private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick: () -> Unit) {
    val interactionSource = remember { MutableInteractionSource() }
    val isFocused by interactionSource.collectIsFocusedAsState()
    Surface(
    ClickableShortcutSurface(
        onClick = onClick,
        shape = RoundedCornerShape(24.dp),
        color = Color.Transparent,
        modifier =
            Modifier.semantics { role = Role.Button }
                .fillMaxWidth()
                .focusable(interactionSource = interactionSource)
        modifier = Modifier.semantics { role = Role.Button }.fillMaxWidth(),
        interactionSource = interactionSource
    ) {
        Row(
            modifier =
+199 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.keyboard.shortcut.ui.composable

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTonalElevationEnabled
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.modifiers.thenIf

/**
 * A selectable surface with no default focus/hover indications.
 *
 * This composable is similar to [androidx.compose.material3.Surface], but removes default
 * focus/hover states to enable custom implementations.
 */
@Composable
@NonRestartableComposable
fun SelectableShortcutSurface(
    selected: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = RectangleShape,
    color: Color = MaterialTheme.colorScheme.surface,
    contentColor: Color = contentColorFor(color),
    tonalElevation: Dp = 0.dp,
    shadowElevation: Dp = 0.dp,
    border: BorderStroke? = null,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable () -> Unit
) {
    @Suppress("NAME_SHADOWING")
    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
    val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
    CompositionLocalProvider(
        LocalContentColor provides contentColor,
        LocalAbsoluteTonalElevation provides absoluteElevation
    ) {
        Box(
            modifier =
                modifier
                    .minimumInteractiveComponentSize()
                    .surface(
                        shape = shape,
                        backgroundColor =
                            surfaceColorAtElevation(color = color, elevation = absoluteElevation),
                        border = border,
                        shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }
                    )
                    .selectable(
                        selected = selected,
                        interactionSource = interactionSource,
                        indication = null,
                        enabled = enabled,
                        onClick = onClick
                    ),
            propagateMinConstraints = true
        ) {
            content()
        }
    }
}

/**
 * A clickable surface with no default focus/hover indications.
 *
 * This composable is similar to [androidx.compose.material3.Surface], but removes default
 * focus/hover states to enable custom implementations.
 */
@Composable
@NonRestartableComposable
fun ClickableShortcutSurface(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = RectangleShape,
    color: Color = MaterialTheme.colorScheme.surface,
    contentColor: Color = contentColorFor(color),
    tonalElevation: Dp = 0.dp,
    shadowElevation: Dp = 0.dp,
    border: BorderStroke? = null,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable () -> Unit
) {
    @Suppress("NAME_SHADOWING")
    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
    val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
    CompositionLocalProvider(
        LocalContentColor provides contentColor,
        LocalAbsoluteTonalElevation provides absoluteElevation
    ) {
        Box(
            modifier =
                modifier
                    .minimumInteractiveComponentSize()
                    .surface(
                        shape = shape,
                        backgroundColor =
                            surfaceColorAtElevation(color = color, elevation = absoluteElevation),
                        border = border,
                        shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }
                    )
                    .clickable(
                        interactionSource = interactionSource,
                        indication = null,
                        enabled = enabled,
                        onClick = onClick
                    ),
            propagateMinConstraints = true
        ) {
            content()
        }
    }
}

@Composable
private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color {
    return MaterialTheme.colorScheme.applyTonalElevation(color, elevation)
}

@Composable
internal fun ColorScheme.applyTonalElevation(backgroundColor: Color, elevation: Dp): Color {
    val tonalElevationEnabled = LocalTonalElevationEnabled.current
    return if (backgroundColor == surface && tonalElevationEnabled) {
        surfaceColorAtElevation(elevation)
    } else {
        backgroundColor
    }
}

/**
 * Applies surface-related modifiers to a composable.
 *
 * This function adds background, border, and shadow effects to a composable. Also ensure the
 * composable is clipped to the given shape.
 *
 * @param shape The shape to apply to the composable's background, border, and clipping.
 * @param backgroundColor The background color to apply to the composable.
 * @param border An optional border to draw around the composable.
 * @param shadowElevation The size of the shadow below the surface. To prevent shadow creep, only
 *   apply shadow elevation when absolutely necessary, such as when the surface requires visual
 *   separation from a patterned background. Note that It will not affect z index of the Surface. If
 *   you want to change the drawing order you can use `Modifier.zIndex`.
 * @return The modified Modifier instance with surface-related modifiers applied.
 */
@Stable
private fun Modifier.surface(
    shape: Shape,
    backgroundColor: Color,
    border: BorderStroke?,
    shadowElevation: Float,
): Modifier {
    return this.thenIf(shadowElevation > 0f) {
            Modifier.graphicsLayer(shadowElevation = shadowElevation, shape = shape, clip = false)
        }
        .thenIf(border != null) { Modifier.border(border!!, shape) }
        .background(color = backgroundColor, shape = shape)
        .clip(shape)
}