Skip to content

Commit 9c53328

Browse files
authored
feat(link): update Link styles and appearance support (#1216)
1 parent 875145a commit 9c53328

19 files changed

Lines changed: 1269 additions & 60 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// LinkExample.swift
3+
// Shared
4+
5+
import Foundation
6+
#if OPENSWIFTUI
7+
import OpenSwiftUI
8+
#else
9+
import SwiftUI
10+
#endif
11+
12+
private let linkDestination = URL(string: "https://example.com")!
13+
14+
struct LinkExample: View {
15+
private let localizedTitle: LocalizedStringKey = "Localized title"
16+
private let title = "String title"
17+
18+
var body: some View {
19+
VStack(alignment: .leading, spacing: 16) {
20+
Link(localizedTitle, destination: linkDestination)
21+
Link(title, destination: linkDestination)
22+
Link(destination: linkDestination) {
23+
HStack(spacing: 8) {
24+
Color.orange
25+
.frame(width: 16, height: 16)
26+
Text("Custom label")
27+
}
28+
}
29+
}
30+
.padding()
31+
}
32+
}
33+
34+
struct LinkButtonStyleExample: View {
35+
var body: some View {
36+
VStack(alignment: .leading, spacing: 16) {
37+
Link("Default style", destination: linkDestination)
38+
Link("Automatic style", destination: linkDestination)
39+
.buttonStyle(.automatic)
40+
Link("Plain style", destination: linkDestination)
41+
.buttonStyle(.plain)
42+
Link("Bordered style", destination: linkDestination)
43+
.buttonStyle(.bordered)
44+
Link("Custom style", destination: linkDestination)
45+
.buttonStyle(ExampleLinkButtonStyle())
46+
#if os(macOS)
47+
Link("Explicit link style", destination: linkDestination)
48+
.buttonStyle(.link)
49+
Button("Link button style") {}
50+
.buttonStyle(.link)
51+
#endif
52+
}
53+
.padding()
54+
}
55+
}
56+
57+
private struct ExampleLinkButtonStyle: ButtonStyle {
58+
func makeBody(configuration: Configuration) -> some View {
59+
configuration.label
60+
.foregroundStyle(.white)
61+
.padding(8)
62+
.background(.purple)
63+
.opacity(configuration.isPressed ? 0.5 : 1)
64+
}
65+
}
66+
67+
struct LinkAppearanceExample: View {
68+
var body: some View {
69+
VStack(alignment: .leading, spacing: 16) {
70+
Link("Default appearance", destination: linkDestination)
71+
Link("Tinted link", destination: linkDestination)
72+
.tint(.purple)
73+
Link("Foreground style", destination: linkDestination)
74+
.foregroundStyle(.orange)
75+
Link(destination: linkDestination) {
76+
Text("Styled label")
77+
.font(.headline)
78+
.foregroundStyle(.green)
79+
}
80+
}
81+
.padding()
82+
}
83+
}
84+
85+
struct LinkShapeStyleExample: View {
86+
var body: some View {
87+
VStack(alignment: .leading, spacing: 16) {
88+
Text("Link foreground style")
89+
.foregroundStyle(.link)
90+
Rectangle()
91+
.fill(.link)
92+
.frame(height: 24)
93+
}
94+
.padding()
95+
}
96+
}
97+
98+
#Preview("LinkExample") {
99+
LinkExample()
100+
}
101+
102+
#Preview("LinkButtonStyleExample") {
103+
LinkButtonStyleExample()
104+
}
105+
106+
#Preview("LinkAppearanceExample") {
107+
LinkAppearanceExample()
108+
}
109+
110+
#Preview("LinkShapeStyleExample") {
111+
LinkShapeStyleExample()
112+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// LinkUITests.swift
3+
// OpenSwiftUIUITests
4+
5+
import SnapshotTesting
6+
import Testing
7+
@testable import TestingHost
8+
9+
@MainActor
10+
@Suite(.snapshots(record: .never, diffTool: diffTool))
11+
struct LinkUITests {
12+
@Test(.tags(.localization))
13+
func initializers() {
14+
openSwiftUIAssertSnapshot(
15+
of: LinkExample(),
16+
size: .init(width: 320, height: 160)
17+
)
18+
}
19+
20+
@Test
21+
func buttonStyles() {
22+
openSwiftUIAssertSnapshot(
23+
of: LinkButtonStyleExample(),
24+
size: .init(width: 320, height: 360)
25+
)
26+
}
27+
28+
@Test(arguments: [false, true])
29+
func appearance(isDisabled: Bool) {
30+
openSwiftUIAssertSnapshot(
31+
of: LinkAppearanceExample()
32+
.disabled(isDisabled),
33+
size: .init(width: 320, height: 200),
34+
named: isDisabled ? "disabled" : "enabled"
35+
)
36+
}
37+
38+
@Test(arguments: [ColorScheme.light, .dark])
39+
func shapeStyle(colorScheme: ColorScheme) {
40+
openSwiftUIAssertSnapshot(
41+
of: LinkShapeStyleExample()
42+
.environment(\.colorScheme, colorScheme),
43+
size: .init(width: 320, height: 120),
44+
named: colorScheme == .dark ? "dark" : "light"
45+
)
46+
}
47+
}

‎Package.resolved‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Sources/OpenSwiftUI/Accessibility/AccessibilityConfiguration.swift‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,36 @@ struct AccessibilityAdjustableModifier<Content, Value>: AccessibilityConfigurati
197197

198198
// TODO: AccessibilityNavigationLinkButtonModifier
199199
// TODO: AccessibilityNavigationLinkButtonModifier.Representable
200-
// TODO: AccessibilityLinkModifier
201-
// TODO: AccessibilityLinkModifier.Configuration
200+
201+
// MARK: - AccessibilityLinkModifier
202+
203+
struct AccessibilityLinkModifier<Content: View>: AccessibilityConfigurationModifier {
204+
struct Configuration {
205+
var destination: LinkDestination
206+
var isArchive: Bool
207+
}
208+
209+
var configuration: Configuration
210+
211+
init(_ destination: LinkDestination, isArchive: Bool) {
212+
configuration = Configuration(destination: destination, isArchive: isArchive)
213+
}
214+
215+
@ViewBuilder
216+
func body(content: Content) -> some View {
217+
if configuration.isArchive {
218+
content
219+
.accessibilityIgnoreViewResponders()
220+
.accessibilityCombinedElement(options: .includeHiddenChild, ignoredTraits: [.isImage, .isStaticText])
221+
.accessibilityAddTraits(.isButton)
222+
.accessibilityRemoveTraits(.isLink)
223+
} else {
224+
content
225+
.accessibility(AccessibilityProperties.LinkDestinationKey.self, configuration.destination.configuration)
226+
.accessibilityAddTraits(.isLink)
227+
}
228+
}
229+
}
202230

203231
// MARK: - AccessibilitySliderModifier
204232

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// LinkShapeStyle.swift
3+
// OpenSwiftUI
4+
//
5+
// Audited for 6.5.4
6+
// Status: Complete
7+
8+
public import OpenSwiftUICore
9+
10+
// MARK: - ShapeStyle + LinkShapeStyle
11+
12+
@available(OpenSwiftUI_v5_0, *)
13+
extension ShapeStyle where Self == LinkShapeStyle {
14+
/// A style for links.
15+
@_alwaysEmitIntoClient
16+
public static var link: LinkShapeStyle {
17+
.init()
18+
}
19+
}
20+
21+
// MARK: - LinkShapeStyle
22+
23+
/// A style for links.
24+
@available(OpenSwiftUI_v5_0, *)
25+
@frozen
26+
public struct LinkShapeStyle: ShapeStyle {
27+
public init() {
28+
_openSwiftUIEmptyStub()
29+
}
30+
31+
public func _apply(to shape: inout _ShapeStyle_Shape) {
32+
#if os(macOS)
33+
AppearanceCatalogShapeStyle(catalogName: "linkColor")._apply(to: &shape)
34+
#else
35+
let isDark = shape.environment.colorScheme == .dark
36+
Color(
37+
.sRGB,
38+
red: isDark ? 9 / 255 : 0,
39+
green: isDark ? 132 / 255 : 122 / 255,
40+
blue: 1
41+
)._apply(to: &shape)
42+
#endif
43+
}
44+
45+
public typealias Resolved = Never
46+
}

‎Sources/OpenSwiftUI/View/ArchivedView/ArchivedView.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ extension _ArchivedViewStates.DeploymentVersion {
241241
extension ViewDecoders {
242242
static func registerStandardArchiveDecoders() {
243243
registerStandard {
244-
// TODO: ArchivableLinkModifier
244+
ArchivableLinkModifier.registerDecoder()
245245
// TODO: TimelineProgressView<ArchivableLinearProgressView.Base>.ArchivableTimelineProgressView
246246
// TODO: TimelineProgressView<CircularPercentageGaugeRing>.ArchivableTimelineProgressView
247247
HandGestureShortcutInteractiveControl.HandGestureShortcutPreferenceWriter.registerDecoder()

‎Sources/OpenSwiftUI/View/Control/Button/ButtonStyle.swift‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,15 @@ extension ButtonStyleModifier {
169169
}
170170
}
171171

172-
// TODO: ArchivableLinkModifier
172+
// MARK: - ArchivableLinkModifier
173+
174+
struct ArchivableLinkModifier: _ArchivableViewModifier {
175+
var configuration: LinkDestination.Configuration
176+
177+
func body(content: Content) -> some View {
178+
content.modifier(LinkButtonModifierBody(destination: LinkDestination(configuration: configuration)))
179+
}
180+
}
173181

174182
// MARK: - WrappedButtonStyle
175183

@@ -193,7 +201,23 @@ extension WrappedButtonStyle: AnyDefaultStyle where Style: AnyDefaultStyle {
193201

194202
// TODO: ArchivableButtonAppIntentModifier
195203

196-
// TODO: LinkButtonModifierBody
204+
// MARK: - LinkButtonModifierBody
205+
206+
private struct LinkButtonModifierBody: ViewModifier {
207+
let destination: LinkDestination
208+
209+
func body(content: Content) -> some View {
210+
ButtonBehavior(action: destination.open) { isPressed in
211+
StaticIf(Semantics.ArchivedLinkIsButton.self) {
212+
content.temporalState(isPressed)
213+
} else: {
214+
content
215+
.modifier(OpacityButtonHighlightModifier(highlighted: isPressed))
216+
.contentShape(Rectangle())
217+
}
218+
}
219+
}
220+
}
197221

198222
// MARK: - ResolvedButtonStyleBody
199223

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// LinkButtonStyle.swift
3+
// OpenSwiftUI
4+
//
5+
// Audited for 6.5.4
6+
// Status: Complete
7+
8+
public import OpenSwiftUICore
9+
10+
// MARK: - PrimitiveButtonStyle + LinkButtonStyle
11+
12+
@available(OpenSwiftUI_v1_0, *)
13+
@available(iOS, unavailable)
14+
@available(tvOS, unavailable)
15+
@available(watchOS, unavailable)
16+
@available(visionOS, unavailable)
17+
extension PrimitiveButtonStyle where Self == LinkButtonStyle {
18+
/// A button style that displays a link.
19+
@_alwaysEmitIntoClient
20+
@MainActor
21+
@preconcurrency
22+
public static var link: LinkButtonStyle {
23+
.init()
24+
}
25+
}
26+
27+
// MARK: - LinkButtonStyle
28+
29+
/// A button style that displays a link.
30+
@available(OpenSwiftUI_v1_0, *)
31+
@available(iOS, unavailable)
32+
@available(tvOS, unavailable)
33+
@available(watchOS, unavailable)
34+
@available(visionOS, unavailable)
35+
@MainActor
36+
@preconcurrency
37+
public struct LinkButtonStyle: PrimitiveButtonStyle {
38+
/// Creates a link button style.
39+
public init() {
40+
_openSwiftUIEmptyStub()
41+
}
42+
43+
public func makeBody(configuration: Configuration) -> some View {
44+
#if os(macOS)
45+
Button(configuration)
46+
.accessibilityAddTraits(.isLink)
47+
.modifier(
48+
PrimitiveButtonStyleContainerModifier(style: PlatformItemListButtonStyle())
49+
.requiring(MenuStyleContext.self)
50+
)
51+
.buttonStyle(AppKitButtonStyle(appearance: .link))
52+
#else
53+
_openSwiftUIPlatformUnimplementedFailure()
54+
#endif
55+
}
56+
}
57+
58+
@available(*, unavailable)
59+
extension LinkButtonStyle: Sendable {}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// WidgetLinkButtonStyle.swift
3+
// OpenSwiftUI
4+
//
5+
// Audited for 6.5.4
6+
// Status: Complete
7+
8+
import OpenSwiftUICore
9+
10+
struct WidgetLinkButtonStyle: ButtonStyle {
11+
func makeBody(configuration: Configuration) -> some View {
12+
configuration.label
13+
.modifier(OpacityButtonHighlightModifier(highlighted: configuration.isPressed))
14+
.contentShape(Rectangle())
15+
}
16+
}

0 commit comments

Comments
 (0)