Skip to content

Commit

Permalink
OGM-2374: New component - Navigation Tab Group (#1929)
Browse files Browse the repository at this point in the history
* OGM-2374: Create components

* OGM-2374: Move components, show in example app

* OGM-2374: Disable force-update selectedIndex

* OGM-2374: Add navigation tab tests

* OGM-2374: Add navigation tab group tests

* OGM-2374: Code review feedback

* OGM-2374: Make BPKText able to inherit foreground

* Revert "OGM-2374: Make BPKText able to inherit foreground"

This reverts commit 2019e8a.

* OGM-2374: Set font for text

* Record snapshots

* Updated snapshots

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
melwaraki and github-actions[bot] committed Apr 5, 2024
1 parent 09680be commit 706c981
Show file tree
Hide file tree
Showing 30 changed files with 503 additions and 0 deletions.
@@ -0,0 +1,25 @@
/*
* Backpack - 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.
*/

import SwiftUI

public extension BPKNavigationTabGroup {
enum Style {
case `default`, onDark
}
}
84 changes: 84 additions & 0 deletions Backpack-SwiftUI/NavigationTab/Classes/BPKNavigationTab.swift
@@ -0,0 +1,84 @@
/*
* Backpack - 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.
*/

import SwiftUI

struct BPKNavigationTab: View {

let text: String
let icon: BPKIcon?
let selected: Bool
let style: BPKNavigationTabGroup.Style
let onClick: () -> Void

init(
_ text: String,
icon: BPKIcon? = nil,
selected: Bool = false,
style: BPKNavigationTabGroup.Style = .default,
onClick: @escaping () -> Void
) {
self.text = text
self.icon = icon
self.selected = selected
self.style = style
self.onClick = onClick
}

var body: some View {

Button(action: onClick) {
HStack(spacing: .md) {
if let icon {
BPKIconView(icon)
}
Text(text)
.font(BPKFontStyle.label2.font)
.lineLimit(1)
}
.padding(.horizontal, .base)
}
.buttonStyle(
NavigationTabStyle(
style: style,
selected: selected
)
)
.accessibilityAddTraits(selected ? [.isSelected] : [])
.if(!BPKFont.enableDynamicType, transform: {
$0.sizeCategory(.large)
})
}
}

struct BPKNavigationTab_Previews: PreviewProvider {
static var previews: some View {
VStack {
HStack {
BPKNavigationTab("Explore", icon: .explore, selected: true) {}
BPKNavigationTab("Flights", icon: .flight) {}
}
HStack {
BPKNavigationTab("Explore", icon: .explore, selected: true, style: .onDark) {}
BPKNavigationTab("Flights", icon: .flight, style: .onDark) {}
}
.padding()
.background(.surfaceContrastColor)
}
}
}
112 changes: 112 additions & 0 deletions Backpack-SwiftUI/NavigationTab/Classes/BPKNavigationTabGroup.swift
@@ -0,0 +1,112 @@
/*
* Backpack - 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.
*/

import SwiftUI

/// A Group of tabs that allows a single tab to be selected at a time.
public struct BPKNavigationTabGroup: View {
private let tabs: [Item]
private let style: BPKNavigationTabGroup.Style
private let onItemClick: (_ index: Int) -> Void

@Binding private var selectedIndex: Int

public init(
tabs: [Item],
style: BPKNavigationTabGroup.Style = .default,
selectedIndex: Binding<Int>,
onItemClick: @escaping (_ index: Int) -> Void
) {
self.tabs = tabs
self.style = style
self._selectedIndex = selectedIndex
self.onItemClick = onItemClick
}

public var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: .md) {
ForEach(Array(tabs.enumerated()), id: \.element) { index, item in
tab(for: item, index: index)
}
}
.padding(1) // to account for chip outlines
}
}

@ViewBuilder
private func tab(for tab: Item, index: Int) -> some View {
BPKNavigationTab(
tab.text,
icon: tab.icon,
selected: selectedIndex == index,
style: style
) {
onItemClick(index)
}
}
}

public extension BPKNavigationTabGroup {
struct Item: Hashable {
let text: String
let icon: BPKIcon?

public init(text: String, icon: BPKIcon? = nil) {
self.text = text
self.icon = icon
}

public static func == (lhs: Item, rhs: Item) -> Bool {
lhs.text == rhs.text && lhs.icon?.name == rhs.icon?.name
}

public func hash(into hasher: inout Hasher) {
hasher.combine(text)
hasher.combine(icon?.name)
}
}
}

struct BPKNavigationTabGroup_Previews: PreviewProvider {

static let tabs: [BPKNavigationTabGroup.Item] = [
.init(text: "Explore", icon: .explore),
.init(text: "Flights", icon: .flight),
.init(text: "Hotels", icon: .hotels),
.init(text: "Car Hire", icon: .cars)
]

static var previews: some View {
VStack {
BPKNavigationTabGroup(
tabs: tabs,
selectedIndex: .constant(0)
) { _ in }
.padding()

BPKNavigationTabGroup(
tabs: tabs,
style: .onDark,
selectedIndex: .constant(0)
) { _ in }
.padding()
.background(.surfaceContrastColor)
}
}
}
70 changes: 70 additions & 0 deletions Backpack-SwiftUI/NavigationTab/Classes/NavigationTabStyle.swift
@@ -0,0 +1,70 @@
/*
* Backpack - 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.
*/

import SwiftUI

struct NavigationTabStyle: ButtonStyle {
let style: BPKNavigationTabGroup.Style
let selected: Bool

func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(minHeight: .xl)
.fixedSize(horizontal: true, vertical: false)
.background(backgroundColor(configuration.isPressed))
.foregroundColor(foregroundColor(configuration.isPressed))
.outline(outlineColor(configuration.isPressed), cornerRadius: .lg)
.clipShape(RoundedRectangle(cornerRadius: .lg))
.if(!BPKFont.enableDynamicType, transform: {
$0.sizeCategory(.large)
})
}

private func outlineColor(_ isPressed: Bool) -> BPKColor {
switch style {
case .`default`:
if selected || isPressed {
return .coreAccentColor
}

return .lineColor
case .onDark:
if selected {
return .coreAccentColor
} else if isPressed {
return .chipOnDarkPressedStrokeColor
} else {
return .lineOnDarkColor
}
}
}

private func backgroundColor(_ isPressed: Bool) -> BPKColor {
return selected ? .coreAccentColor : .clear
}

private func foregroundColor(_ isPressed: Bool) -> BPKColor {
if selected {
return .textPrimaryInverseColor
} else if isPressed {
return style == .default ? .buttonLinkPressedForegroundColor : .buttonLinkOnDarkPressedForegroundColor
} else {
return style == .default ? .textPrimaryColor : .textOnDarkColor
}
}
}
@@ -0,0 +1,61 @@
/*
* Backpack - 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.
*/

import XCTest
import SwiftUI
@testable import Backpack_SwiftUI

class BPKNavigationTabGroupTests: XCTestCase {

private let tabs: [BPKNavigationTabGroup.Item] = [
.init(text: "Explore", icon: .explore),
.init(text: "Flights", icon: .flight),
.init(text: "Hotels", icon: .hotels),
.init(text: "Car Hire", icon: .cars)
]

func test_default() {
assertSnapshot(
VStack {
BPKNavigationTabGroup(
tabs: tabs,
selectedIndex: .constant(0),
onItemClick: { _ in }
)
.padding()
}
.frame(width: 300)
)
}

func test_dark() {
assertSnapshot(
VStack {
BPKNavigationTabGroup(
tabs: tabs,
style: .onDark,
selectedIndex: .constant(0),
onItemClick: { _ in }
)
.padding()
.background(.surfaceContrastColor)
}
.frame(width: 300)
)
}
}

0 comments on commit 706c981

Please sign in to comment.