Skip to content

Commit

Permalink
LMN-525 Implement App search modal (#1756)
Browse files Browse the repository at this point in the history
* LMN-525 Implement App search modal

* added license

* LMN-525 added tests

* fix accessibility for loading

* Updated snapshots for 'rtl'

* Updated snapshots for 'dm'

* Updated snapshots for 'default'

* LMN-525 updates based on feedback

* LMN-525 update the documentation

* added license

* Updated snapshots for 'rtl'

* Updated snapshots for 'dm'

* LMN-525 final updates

* Updated snapshots for 'dm'

* Updated snapshots for 'default'

* Updated snapshots for 'rtl'

* trigger ci

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
LokmaneKrizou and github-actions[bot] authored Oct 10, 2023
1 parent 9fb5db6 commit e8c3583
Show file tree
Hide file tree
Showing 30 changed files with 895 additions and 1 deletion.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Backpack for Android - Skyscanner's Design System
*
* Copyright 2018 Skyscanner Ltd
*
* 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 net.skyscanner.backpack.compose.appsearchmodal

import androidx.compose.runtime.Composable
import androidx.compose.ui.test.isDialog
import net.skyscanner.backpack.compose.BpkSnapshotTest
import net.skyscanner.backpack.demo.compose.AppSearchModalStoryContent
import net.skyscanner.backpack.demo.compose.AppSearchModalStoryContentInputText
import net.skyscanner.backpack.demo.compose.AppSearchModalStoryError
import net.skyscanner.backpack.demo.compose.AppSearchModalStoryLoading
import org.junit.Test

class BpkAppSearchModalTest : BpkSnapshotTest() {

@Test
fun content() = record {
AppSearchModalStoryContent()
}

@Test
fun content_inputText() = record {
AppSearchModalStoryContentInputText()
}

@Test
fun loading() = record {
AppSearchModalStoryLoading()
}

@Test
fun error() = record {
AppSearchModalStoryError()
}

private fun record(
content: @Composable () -> Unit,
) {
snap(comparison = { name ->
compareScreenshot(onNode(isDialog()), name)
}) {
content()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Backpack for Android - Skyscanner's Design System
*
* Copyright 2018 Skyscanner Ltd
*
* 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 net.skyscanner.backpack.demo.components

import net.skyscanner.backpack.meta.ComponentMarker

@ComponentMarker("AppSearchModal")
annotation class AppSearchModalComponent
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/**
* Backpack for Android - Skyscanner's Design System
*
* Copyright 2018 Skyscanner Ltd
*
* 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 net.skyscanner.backpack.demo.compose

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import net.skyscanner.backpack.compose.appsearchmodal.BpkAction
import net.skyscanner.backpack.compose.appsearchmodal.BpkAppSearchModal
import net.skyscanner.backpack.compose.appsearchmodal.BpkAppSearchModalResult
import net.skyscanner.backpack.compose.appsearchmodal.BpkItem
import net.skyscanner.backpack.compose.appsearchmodal.BpkSection
import net.skyscanner.backpack.compose.appsearchmodal.BpkSectionHeading
import net.skyscanner.backpack.compose.appsearchmodal.BpkShortcut
import net.skyscanner.backpack.compose.button.BpkButton
import net.skyscanner.backpack.compose.icon.BpkIcon
import net.skyscanner.backpack.compose.tokens.Airports
import net.skyscanner.backpack.compose.tokens.City
import net.skyscanner.backpack.compose.tokens.Landmark
import net.skyscanner.backpack.compose.tokens.UseLocation
import net.skyscanner.backpack.demo.R
import net.skyscanner.backpack.demo.components.AppSearchModalComponent
import net.skyscanner.backpack.demo.meta.ComposeStory
import net.skyscanner.backpack.meta.StoryKind

@Composable
@AppSearchModalComponent
@ComposeStory("Content")
fun AppSearchModalStoryContent(modifier: Modifier = Modifier) {
AppSearchModalStory(result = contentResult())
}

@Composable
@AppSearchModalComponent
@ComposeStory("Content-InputText", kind = StoryKind.DemoOnly)
fun AppSearchModalStoryContentInputText(modifier: Modifier = Modifier) {
AppSearchModalStory(result = contentResult(), inputText = stringResource(id = R.string.city_rio))
}

@Composable
@AppSearchModalComponent
@ComposeStory("Loading")
fun AppSearchModalStoryLoading(modifier: Modifier = Modifier) {
AppSearchModalStory(result = loadingResult(), inputText = stringResource(id = R.string.city_dubai))
}

@Composable
@AppSearchModalComponent
@ComposeStory("Error")
fun AppSearchModalStoryError(modifier: Modifier = Modifier) {
AppSearchModalStory(result = errorResult())
}

@Composable
private fun AppSearchModalStory(
result: BpkAppSearchModalResult,
modifier: Modifier = Modifier,
inputText: String = "",
) {
Column(modifier.fillMaxSize()) {
DefaultAppSearchModalSample(result = result, inputText)
}
}

@Composable
internal fun DefaultAppSearchModalSample(
result: BpkAppSearchModalResult,
inputText: String,
modifier: Modifier = Modifier,
) {
val destination = remember { mutableStateOf(inputText) }
val showModal = rememberSaveable { mutableStateOf(true) }
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
BpkButton(
text = stringResource(R.string.generic_show),
onClick = { showModal.value = true },
)
}
if (showModal.value) {
BpkAppSearchModal(
modifier = modifier,
title = stringResource(id = R.string.destination),
inputText = destination.value,
inputHint = stringResource(id = R.string.text_field_hint),
results = result,
closeAccessibilityLabel = stringResource(id = R.string.navigation_close),
onClose = { showModal.value = false },
onInputChanged = { destination.value = it },
)
}
}

@Composable
internal fun contentResult() = BpkAppSearchModalResult.Content(
sections = listOf(
BpkSection(
items = listOf(
BpkItem(
title = buildAnnotatedString {
append(stringResource(id = R.string.current_location_title))
},
subtitle = stringResource(id = R.string.current_location_subtitle),
icon = BpkIcon.UseLocation,
) {},
),
),
BpkSection(
headings = BpkSectionHeading(
title = stringResource(id = R.string.recent_searches),
action = BpkAction(text = stringResource(id = R.string.view_more)) {},
),
items = listOf(
BpkItem(
title = buildAnnotatedString { append(stringResource(id = R.string.city_london)) },
subtitle = stringResource(id = R.string.search_modal_item_subtitle),
icon = BpkIcon.City,
onItemSelected = {},

),
BpkItem(
title = buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(id = R.string.city_london))
}
append(" Heathrow")
},
subtitle = stringResource(id = R.string.search_modal_item_subtitle),
icon = BpkIcon.Airports,
onItemSelected = {},

),
BpkItem(
title = buildAnnotatedString { append(stringResource(id = R.string.city_rome)) },
subtitle = stringResource(id = R.string.search_modal_item_subtitle),
icon = BpkIcon.Airports,
onItemSelected = {},

),
),
),
BpkSection(
headings = BpkSectionHeading(
title = stringResource(id = R.string.popular_destinations),
),
items = listOf(
BpkItem(
title = buildAnnotatedString { append(stringResource(id = R.string.city_shenzhen)) },
subtitle = stringResource(id = R.string.search_modal_item_subtitle),
icon = BpkIcon.City,
onItemSelected = {},

),
BpkItem(
title = buildAnnotatedString { append(stringResource(id = R.string.city_paris)) },
subtitle = stringResource(id = R.string.search_modal_item_subtitle),
icon = BpkIcon.City,
onItemSelected = {},

),
BpkItem(
title = buildAnnotatedString { append(stringResource(id = R.string.city_algiers)) },
subtitle = stringResource(id = R.string.search_modal_item_subtitle),
icon = BpkIcon.City,
onItemSelected = {},

),
BpkItem(
title = buildAnnotatedString { append(stringResource(id = R.string.city_madrid)) },
subtitle = stringResource(id = R.string.search_modal_item_subtitle),
icon = BpkIcon.City,
onItemSelected = {},

),
),
),
),
shortcuts = listOf(
BpkShortcut(
stringResource(id = R.string.city_tokyo),
icon = BpkIcon.Landmark,
) {},
BpkShortcut(
stringResource(id = R.string.city_cairo),
icon = BpkIcon.Landmark,
) {},
BpkShortcut(
stringResource(id = R.string.city_long_name),
icon = BpkIcon.Landmark,
) {},
),
)

@Composable
internal fun errorResult() = BpkAppSearchModalResult.Error(
title = stringResource(id = R.string.error_view_title),
description = stringResource(id = R.string.error_view_subtitle),
image = {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
)
},
action = BpkAction(text = stringResource(id = R.string.try_again), onActionSelected = {}),

)

@Composable
internal fun loadingResult() = BpkAppSearchModalResult.Loading(accessibilityLabel = stringResource(id = R.string.content_is_loading))
13 changes: 12 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,18 @@
<string name="city_dubai">Dubai</string>
<string name="city_long_name">A city with long name in mars</string>
<string name="city_rio">Rio de janeiro</string>

<string name="recent_searches">Recent Searches</string>
<string name="popular_destinations">Popular Destinations</string>
<string name="search_modal_item_subtitle">City, United Kingdom</string>
<string name="view_more">View More</string>
<string name="try_again">Try Again</string>
<string name="destination">Destination</string>
<string name="text_field_hint">Enter your next destination</string>
<string name="current_location_title">Current Location</string>
<string name="current_location_subtitle">Use current location</string>
<string name="content_is_loading">Content is Loading</string>
<string name="error_view_title">Looks like you drifted away</string>
<string name="error_view_subtitle">Please check your internet connection to find your way back</string>
<string name="drop_down_action">Drop down action invoked</string>
<string name="dismiss_action">Dismiss action invoked</string>
<string name="sticky_chip_action">Sticky Chip action invoked</string>
Expand Down
Loading

0 comments on commit e8c3583

Please sign in to comment.