Skip to content

Commit

Permalink
Add app search modal (#1820)
Browse files Browse the repository at this point in the history
* Add app search modal

* Update README.md

* Use BPKSpacing.none instead of 0

* Create a State for inputText

* Use paddings instead of fixed height

* Align the background color with Android
  • Loading branch information
iHandle authored Nov 20, 2023
1 parent 8070138 commit c9037b4
Show file tree
Hide file tree
Showing 27 changed files with 893 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018-2023 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.
*/

import SwiftUI

struct AppSearchModalContentView: View {
let state: BPKAppSearchModalContent
var body: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: .base) {
if let shortcuts = state.shortcuts {
makeShortcuts(shortcuts)
}
ForEach(state.sections, id: \.self) {
makeSections($0)
}
}
}
}

private func makeShortcuts(
_ shortcuts: [BPKAppSearchModalContent.Shortcut]
) -> some View {
BPKSingleSelectChipGroup(
chips: shortcuts.map({ BPKSingleSelectChipGroup.ChipItem( text: $0.text, icon: $0.icon) }),
selectedIndex: .constant(nil),
onItemClick: { shortcuts[$0].onShortcutSelected() }
)
}

private func makeSections(_ section: BPKAppSearchModalContent.Section) -> some View {
VStack(spacing: BPKSpacing.none) {
if let heading = section.heading {
HStack {
BPKText(heading.title, style: .label1)
.accessibilityAddTraits(.isHeader)
Spacer()
if let action = heading.action {
BPKButton(action.text, action: action.onActionSelected)
.buttonStyle(.link)
}
}
}
ForEach(section.items, id: \.self) { item in
HStack(spacing: .base) {
BPKIconView(item.icon, size: .large)
VStack(alignment: .leading) {
BPKText(item.title, style: .bodyDefault)
BPKText(item.subtitle, style: .footnote)
}
.padding(.vertical, .base)
Spacer()
}
.onTapGesture(perform: item.onItemSelected)
.accessibilityElement(children: .combine)
}
}
}
}

struct AppSearchModalContentView_Previews: PreviewProvider {
static var previews: some View {
AppSearchModalContentView(state: .init(
sections: (0..<3).map(buildSection),
shortcuts: (0..<4).map(buildShortcut)
))
}

static func buildSection(with index: Int) -> BPKAppSearchModalContent.Section {
return .init(
heading: .init(title: "Section \(index + 1)", action: .init(text: "Action", onActionSelected: { })),
items: (0..<3).map(buildItem)
)
}

static func buildItem(with index: Int) -> BPKAppSearchModalContent.Item {
return .init(
title: "Item No.\(index + 1)",
subtitle: "This is item No.\(index + 1)",
icon: .recentSearches,
onItemSelected: {}
)
}

static func buildShortcut(with index: Int) -> BPKAppSearchModalContent.Shortcut {
return .init(text: "Shortcut \(index + 1)", icon: .landmark, onShortcutSelected: { })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018-2023 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.
*/

import SwiftUI

struct AppSearchModalErrorView: View {
let state: BPKAppSearchModalError
private let imageHeight: CGFloat = 200
private let imageWidth: CGFloat = 277

var body: some View {
VStack {
Spacer()
state.image
.resizable()
.frame(width: imageWidth, height: imageHeight)
.padding(.bottom, 82)
.accessibilityHidden(true)
VStack(spacing: .base) {
BPKText(state.title, style: .heading4)
.accessibilityAddTraits(.isHeader)
BPKText(state.description, style: .footnote)
.lineLimit(nil)
.multilineTextAlignment(.center)
BPKButton(state.action.text, size: .large, action: state.action.onActionSelected)
.stretchable()
}
Spacer()
}
}
}

struct AppSearchModalErrorView_Previews: PreviewProvider {
static var previews: some View {
AppSearchModalErrorView(
state: .init(
title: "Title",
description: "This is the subtitle",
action: .init( text: "Title for button", onActionSelected: { }),
image: Image(systemName: "photo")
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018-2023 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.
*/

import SwiftUI

struct AppSearchModalLoadingView: View {
let state: BPKAppSearchModalLoading
private let skeletonCount: Int = 10

var body: some View {
ScrollView(showsIndicators: false) {
HStack {
VStack(alignment: .leading) {
ForEach((0..<skeletonCount), id: \.self) { _ in
BPKSkeleton.bodytext()
.shimmering()
.padding(.vertical, .base)
}
}
Spacer()
}
}
.accessibilityLabel(state.accessibilityLabel)
}
}

struct AppSearchModalLoadingView_Previews: PreviewProvider {
static var previews: some View {
AppSearchModalLoadingView(
state: .init(accessibilityLabel: "Close")
)
}
}
153 changes: 153 additions & 0 deletions Backpack-SwiftUI/AppSearchModal/Classes/BPKAppSearchModal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018-2023 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.
*/

import SwiftUI

public struct BPKAppSearchModal: View {
let title: String
@Binding var inputText: String
let inputHint: String
let results: BPKAppSearchModalResults
let closeAccessibilityLabel: String
let onClose: () -> Void

public init(
title: String,
inputText: Binding<String>,
inputHint: String,
results: BPKAppSearchModalResults,
closeAccessibilityLabel: String,
onClose: @escaping () -> Void
) {
self.title = title
self._inputText = inputText
self.inputHint = inputHint
self.results = results
self.closeAccessibilityLabel = closeAccessibilityLabel
self.onClose = onClose
}

public var body: some View {
VStack(spacing: .base) {

makeNavigationBar(title: title, closeAccessibilityLabel: closeAccessibilityLabel, onClose: onClose)

switch results {
case .loading(let loading):
BPKTextField(placeholder: inputHint, $inputText)
AppSearchModalLoadingView(state: loading)
case .content(let content):
BPKTextField(placeholder: inputHint, $inputText)
AppSearchModalContentView(state: content)
.padding(.top, .md)
case .error(let error):
AppSearchModalErrorView(state: error)
.padding(.horizontal, .md)
}
}
.padding(.horizontal, .base)
.padding(.top, .base)
.padding(.bottom, BPKSpacing.none)
.background(.surfaceDefaultColor)
}

func makeNavigationBar(
title: String,
closeAccessibilityLabel: String,
onClose: @escaping () -> Void
) -> some View {
ZStack {
HStack {
BPKIconView(.close, size: .large)
.onTapGesture(perform: onClose)
.accessibilityHidden(false)
.accessibilityRemoveTraits(.isImage)
.accessibilityAddTraits(.isButton)
.accessibilityLabel(closeAccessibilityLabel)
Spacer()
}
BPKText(title, style: .heading5)
.padding(.vertical, .sm)
.accessibilityAddTraits(.isHeader)
}
.padding(.vertical, .md)
}
}

struct BPKAppSearchModal_Previews: PreviewProvider {
static var previews: some View {
BPKAppSearchModal(
title: "Search Modal",
inputText: .constant(""),
inputHint: "Search",
results: .content(.init(
sections: (0..<3).map(buildSection),
shortcuts: (0..<4).map(buildShortcut)
)),
closeAccessibilityLabel: "Close",
onClose: { }
)
.previewDisplayName("Content")

BPKAppSearchModal(
title: "Search Modal",
inputText: .constant("Text"),
inputHint: "Search",
results: .loading(.init(accessibilityLabel: "Loading")),
closeAccessibilityLabel: "Close",
onClose: { }
)
.previewDisplayName("Loading")

BPKAppSearchModal(
title: "Search Modal",
inputText: .constant(""),
inputHint: "Search",
results: .error( .init(
title: "Title",
description: "This is the subtitle",
action: .init( text: "Title for button", onActionSelected: { }),
image: Image(systemName: "photo")

)),
closeAccessibilityLabel: "Close",
onClose: { }
)
.previewDisplayName("Error")
}

static func buildSection(with index: Int) -> BPKAppSearchModalContent.Section {
return .init(
heading: .init(title: "Section \(index + 1)", action: .init(text: "Action", onActionSelected: { })),
items: (0..<1).map(buildItem)
)
}

static func buildItem(with index: Int) -> BPKAppSearchModalContent.Item {
return .init(
title: "Item No.\(index + 1)",
subtitle: "This is item No.\(index + 1)",
icon: .recentSearches,
onItemSelected: {}
)
}

static func buildShortcut(with index: Int) -> BPKAppSearchModalContent.Shortcut {
return .init(text: "Shortcut \(index + 1)", icon: .landmark, onShortcutSelected: { })
}
}
Loading

0 comments on commit c9037b4

Please sign in to comment.