@@ -9,85 +9,41 @@ import SwiftUI
99
1010struct RoadmapVoteButton : View {
1111 @State var viewModel : RoadmapFeatureViewModel
12- @Environment ( \. dynamicTypeSize) var typeSize
12+ @Environment ( \. dynamicTypeSize) private var typeSize
1313
1414 @State private var isHovering = false
1515 @State private var showNumber = false
1616 @State private var hasVoted = false
1717
1818 var body : some View {
1919 Button {
20- if viewModel. canVote {
21- Task {
22- if !viewModel. feature. hasVoted {
23- await viewModel. vote ( )
24- } else {
25- await viewModel. unvote ( )
26- }
27- #if os(iOS)
28- let haptic = UIImpactFeedbackGenerator ( style: . soft)
29- haptic. impactOccurred ( )
30- #endif
20+ guard viewModel. canVote else { return }
21+ Task {
22+ if !viewModel. feature. hasVoted {
23+ await viewModel. vote ( )
24+ announceAccessibility ( " Added vote " )
25+ } else {
26+ await viewModel. unvote ( )
27+ announceAccessibility ( " Removed vote " )
3128 }
29+ #if os(iOS)
30+ UIImpactFeedbackGenerator ( style: . soft) . impactOccurred ( )
31+ #endif
3232 }
3333 } label: {
3434 ZStack {
3535 if typeSize. isAccessibilitySize {
3636 HStack ( spacing: isHovering ? 2 : 0 ) {
37- if viewModel. canVote {
38- if !viewModel. feature. hasVoted {
39- viewModel. configuration. style. upvoteIcon
40- . foregroundColor ( hasVoted ? viewModel. configuration. style. selectedForegroundColor : viewModel. configuration. style. tintColor)
41- . imageScale ( . large)
42- . font ( Font . system ( size: 17 , weight: . medium) )
43- . frame ( maxWidth: 24 , maxHeight: 24 )
44- } else {
45- viewModel. configuration. style. unvoteIcon
46- . foregroundColor ( hasVoted ? viewModel. configuration. style. selectedForegroundColor : viewModel. configuration. style. tintColor)
47- . imageScale ( . large)
48- . font ( Font . system ( size: 17 , weight: . medium) )
49- . frame ( maxWidth: 24 , maxHeight: 24 )
50- }
51- }
52-
53- if showNumber {
54- Text ( " \( viewModel. voteCount) " )
55- . lineLimit ( 1 )
56- . foregroundColor ( hasVoted ? viewModel. configuration. style. selectedForegroundColor : viewModel. configuration. style. tintColor)
57- . minimumScaleFactor ( 0.5 )
58- . font ( viewModel. configuration. style. numberFont)
59- }
37+ icon. accessibilityHidden ( true )
38+ if showNumber { numberText }
6039 }
6140 . padding ( viewModel. configuration. style. radius)
6241 . frame ( minHeight: 64 )
6342 . background ( backgroundView)
6443 } else {
6544 VStack ( spacing: isHovering ? 6 : 4 ) {
66- if viewModel. canVote {
67- if !viewModel. feature. hasVoted {
68- viewModel. configuration. style. upvoteIcon
69- . foregroundColor ( hasVoted ? viewModel. configuration. style. selectedForegroundColor : viewModel. configuration. style. tintColor)
70- . imageScale ( . large)
71- . font ( viewModel. configuration. style. numberFont)
72- . frame ( maxWidth: 20 , maxHeight: 20 )
73- . minimumScaleFactor ( 0.75 )
74- } else {
75- viewModel. configuration. style. unvoteIcon
76- . foregroundColor ( hasVoted ? viewModel. configuration. style. selectedForegroundColor : viewModel. configuration. style. tintColor)
77- . imageScale ( . large)
78- . font ( viewModel. configuration. style. numberFont)
79- . frame ( maxWidth: 20 , maxHeight: 20 )
80- . minimumScaleFactor ( 0.75 )
81- }
82- }
83-
84- if showNumber {
85- Text ( " \( viewModel. voteCount) " )
86- . lineLimit ( 1 )
87- . foregroundColor ( hasVoted ? viewModel. configuration. style. selectedForegroundColor : viewModel. configuration. style. tintColor)
88- . font ( viewModel. configuration. style. numberFont)
89- . minimumScaleFactor ( 0.9 )
90- }
45+ icon. accessibilityHidden ( true )
46+ if showNumber { numberText }
9147 }
9248 . frame ( minWidth: 56 )
9349 . frame ( height: 64 )
@@ -102,39 +58,86 @@ struct RoadmapVoteButton: View {
10258 . buttonBorderShape( . roundedRectangle( radius: viewModel. configuration. style. radius) )
10359 . clipShape ( RoundedRectangle ( cornerRadius: viewModel. configuration. style. radius, style: . continuous) )
10460 #endif
61+ . accessibilityElement( children: . ignore)
62+ . accessibilityLabel ( Text ( " Vote " ) )
63+ . accessibilityValue ( Text ( accessibilityValue) )
64+ . accessibilityHint ( Text ( viewModel. canVote
65+ ? ( hasVoted
66+ ? " Double-tap to remove your vote for \( viewModel. feature. localizedFeatureTitle) "
67+ : " Double-tap to vote for \( viewModel. feature. localizedFeatureTitle) " )
68+ : " Voting unavailable " ) )
69+ . accessibilityAddTraits ( . isButton)
70+ . accessibilityAddTraits ( hasVoted ? . isSelected : [ ] )
71+ . accessibilityRespondsToUserInteraction ( viewModel. canVote)
72+ . accessibilityShowsLargeContentViewer ( )
73+ . accessibilityIdentifier ( " roadmap_vote_button " )
74+ . disabled ( !viewModel. canVote)
10575 . onChange ( of: viewModel. voteCount) { _, newCount in
10676 if newCount > 0 {
107- withAnimation ( . spring( response: 0.45 , dampingFraction: 0.4 , blendDuration : 0 ) ) {
77+ withAnimation ( . spring( response: 0.45 , dampingFraction: 0.4 ) ) {
10878 showNumber = true
10979 }
11080 }
11181 }
11282 . onChange ( of: viewModel. feature. hasVoted) { _, newVote in
113- withAnimation ( . spring( response: 0.45 , dampingFraction: 0.4 , blendDuration : 0 ) ) {
83+ withAnimation ( . spring( response: 0.45 , dampingFraction: 0.4 ) ) {
11484 hasVoted = newVote
11585 }
11686 }
11787 . onHover { newHover in
11888 if viewModel. canVote && !hasVoted {
119- withAnimation ( . spring( response: 0.4 , dampingFraction: 0.7 , blendDuration : 0 ) ) {
89+ withAnimation ( . spring( response: 0.4 , dampingFraction: 0.7 ) ) {
12090 isHovering = newHover
12191 }
12292 }
12393 }
12494 . onAppear {
12595 showNumber = viewModel. voteCount > 0
126- withAnimation ( . spring( response: 0.45 , dampingFraction: 0.4 , blendDuration : 0 ) ) {
96+ withAnimation ( . spring( response: 0.45 , dampingFraction: 0.4 ) ) {
12797 hasVoted = viewModel. feature. hasVoted
12898 }
12999 }
130- . accessibilityHint ( viewModel. canVote ? !viewModel. feature. hasVoted ? Text ( " Vote for \( viewModel. feature. localizedFeatureTitle) " ) : Text ( " Remove vote for \( viewModel. feature. localizedFeatureTitle) " ) : Text ( " " ) )
131- . help ( viewModel. canVote ? !viewModel. feature. hasVoted ? Text ( " Vote for \( viewModel. feature. localizedFeatureTitle) " ) : Text ( " Remove vote for \( viewModel. feature. localizedFeatureTitle) " ) : Text ( " " ) )
100+ . help ( viewModel. canVote
101+ ? ( !viewModel. feature. hasVoted
102+ ? Text ( " Vote for \( viewModel. feature. localizedFeatureTitle) " )
103+ : Text ( " Remove vote for \( viewModel. feature. localizedFeatureTitle) " ) )
104+ : Text ( " Voting unavailable " ) )
132105 . animateAccessible ( )
133- . accessibilityShowsLargeContentViewer ( )
106+ }
107+
108+ // MARK: - Pieces
109+
110+ private var icon : some View {
111+ Group {
112+ if viewModel. canVote {
113+ if !viewModel. feature. hasVoted {
114+ viewModel. configuration. style. upvoteIcon
115+ } else {
116+ viewModel. configuration. style. unvoteIcon
117+ }
118+ }
119+ }
120+ . foregroundColor ( hasVoted ? viewModel. configuration. style. selectedForegroundColor
121+ : viewModel. configuration. style. tintColor)
122+ . imageScale ( . large)
123+ . font ( typeSize. isAccessibilitySize ? . system( size: 17 , weight: . medium)
124+ : viewModel. configuration. style. numberFont)
125+ . frame ( maxWidth: typeSize. isAccessibilitySize ? 24 : 20 ,
126+ maxHeight: typeSize. isAccessibilitySize ? 24 : 20 )
127+ . minimumScaleFactor ( 0.75 )
128+ }
129+
130+ private var numberText : some View {
131+ Text ( " \( viewModel. voteCount) " )
132+ . lineLimit ( 1 )
133+ . foregroundColor ( hasVoted ? viewModel. configuration. style. selectedForegroundColor
134+ : viewModel. configuration. style. tintColor)
135+ . font ( viewModel. configuration. style. numberFont)
136+ . minimumScaleFactor ( typeSize. isAccessibilitySize ? 0.5 : 0.9 )
134137 }
135138
136139 @ViewBuilder
137- var overlayBorder : some View {
140+ private var overlayBorder : some View {
138141 if isHovering {
139142 RoundedRectangle ( cornerRadius: viewModel. configuration. style. radius, style: . continuous)
140143 . stroke ( viewModel. configuration. style. tintColor, lineWidth: 1 )
@@ -146,4 +149,19 @@ struct RoadmapVoteButton: View {
146149 . opacity ( hasVoted ? 1 : 0.1 )
147150 . clipShape ( RoundedRectangle ( cornerRadius: viewModel. configuration. style. radius, style: . continuous) )
148151 }
152+
153+ // MARK: - A11y helpers
154+
155+ private var accessibilityValue : String {
156+ let c = viewModel. voteCount
157+ if c == 0 { return " 0 votes " }
158+ if c == 1 { return " 1 vote " }
159+ return " \( c) votes "
160+ }
161+
162+ private func announceAccessibility( _ text: String ) {
163+ #if os(iOS)
164+ UIAccessibility . post ( notification: . announcement, argument: text)
165+ #endif
166+ }
149167}
0 commit comments