Skip to content

Commit 4c0c69b

Browse files
authored
[macOS] Announce DatePickerController changes in VoiceOver (#2134)
* Announce changes to Date Picker * Clean up Date Picker demo * Use for loops * Switch to using NSAccessibilityStepper
1 parent b11d3c3 commit 4c0c69b

File tree

2 files changed

+97
-67
lines changed

2 files changed

+97
-67
lines changed

Demos/FluentUIDemo_macOS/FluentUITestViewControllers/TestDatePickerController.swift

+30-42
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ class TestDatePickerController: NSViewController {
1414
let calendar = Calendar.current
1515
let date = calendar.date(from: DateComponents(year: 2019, month: 12, day: 9))
1616

17-
datePickerController = DatePickerController(date: date, calendar: calendar, style: .dateTime)
18-
menuDatePickerController = DatePickerController(date: date, calendar: calendar, style: .dateTime)
19-
20-
datePickerController?.delegate = self
21-
menuDatePickerController?.delegate = self
22-
23-
datePickerController?.hasEdgePadding = true
24-
menuDatePickerController?.hasEdgePadding = true
17+
for location in DatePickerLocation.allCases {
18+
let datePickerController = DatePickerController(date: date, calendar: calendar, style: .dateTime)
19+
datePickerController.delegate = self
20+
datePickerController.hasEdgePadding = true
21+
datePickerControllers[location] = datePickerController
22+
}
2523
}
2624

2725
@available(*, unavailable)
@@ -50,7 +48,7 @@ class TestDatePickerController: NSViewController {
5048
vfxView.layer?.cornerRadius = 5
5149

5250
horizontalStack.addView(vfxView, in: .center)
53-
if let controller = datePickerController {
51+
if let controller = datePickerControllers[.inline] {
5452
vfxView.addSubview(controller.view)
5553

5654
NSLayoutConstraint.activate([
@@ -70,7 +68,7 @@ class TestDatePickerController: NSViewController {
7068
let menu = NSMenu()
7169
datePickerMenuItem = NSMenuItem(title: "NSMenu", action: nil, keyEquivalent: "")
7270

73-
if let controller = menuDatePickerController, let menuItem = datePickerMenuItem {
71+
if let controller = datePickerControllers[.menu], let menuItem = datePickerMenuItem {
7472
menuItem.view = NSView(frame: NSRect(origin: .zero, size: controller.view.fittingSize))
7573
menuItem.view?.addSubview(controller.view)
7674
menu.addItem(menuItem)
@@ -128,8 +126,8 @@ class TestDatePickerController: NSViewController {
128126
let delegateMessagesScrollView = NSScrollView()
129127
delegateMessagesScrollView.documentView = delegateMessagesTextView
130128

131-
[delegateMessagesLabel, delegateMessagesScrollView].forEach {
132-
containerView.addView($0, in: .bottom)
129+
for view in [delegateMessagesLabel, delegateMessagesScrollView] {
130+
containerView.addView(view, in: .bottom)
133131
}
134132

135133
NSLayoutConstraint.activate([
@@ -145,8 +143,7 @@ class TestDatePickerController: NSViewController {
145143
}
146144

147145
@objc func clearCustomColor() {
148-
datePickerController?.customSelectionColor = nil
149-
menuDatePickerController?.customSelectionColor = nil
146+
datePickerControllers.values.forEach { $0.customSelectionColor = nil }
150147
}
151148

152149
@objc func launchColorPicker() {
@@ -157,54 +154,41 @@ class TestDatePickerController: NSViewController {
157154
}
158155

159156
@objc func toggleSecondaryCalendar() {
160-
if datePickerController?.secondaryCalendar == nil {
161-
datePickerController?.secondaryCalendar = chineseLunarCalendar
162-
} else {
163-
datePickerController?.secondaryCalendar = nil
164-
}
165-
166-
if menuDatePickerController?.secondaryCalendar == nil {
167-
menuDatePickerController?.secondaryCalendar = chineseLunarCalendar
168-
} else {
169-
menuDatePickerController?.secondaryCalendar = nil
157+
for datePickerController in datePickerControllers.values {
158+
if datePickerController.secondaryCalendar == nil {
159+
datePickerController.secondaryCalendar = chineseLunarCalendar
160+
} else {
161+
datePickerController.secondaryCalendar = nil
162+
}
170163
}
171164
}
172165

173166
@objc func toggleAutoSelection(_ sender: NSButton) {
174167
let enabled = sender.state == .on
175-
datePickerController?.autoSelectWhenPaging = enabled
176-
menuDatePickerController?.autoSelectWhenPaging = enabled
168+
datePickerControllers.values.forEach { $0.autoSelectWhenPaging = enabled }
177169
}
178170

179171
@objc func toggleEdgePadding(_ sender: NSButton) {
180172
let enabled = sender.state == .on
181-
datePickerController?.hasEdgePadding = enabled
182-
menuDatePickerController?.hasEdgePadding = enabled
183-
datePickerMenuItem?.view?.frame.size = menuDatePickerController?.view.fittingSize ?? .zero
173+
datePickerControllers.values.forEach { $0.hasEdgePadding = enabled }
174+
datePickerMenuItem?.view?.frame.size = datePickerControllers[.menu]?.view.fittingSize ?? .zero
184175
}
185176

186177
@objc func toggleTextDatePicker(_ sender: NSButton) {
187178
let enabled = sender.state == .on
188-
datePickerController?.hasTextField = enabled
189-
menuDatePickerController?.hasTextField = enabled
190-
datePickerMenuItem?.view?.frame.size = menuDatePickerController?.view.fittingSize ?? .zero
179+
datePickerControllers.values.forEach { $0.hasTextField = enabled }
180+
datePickerMenuItem?.view?.frame.size = datePickerControllers[.menu]?.view.fittingSize ?? .zero
191181
}
192182

193183
@objc func showPopover(_ sender: NSButton) {
194184
let popover = NSPopover()
195185
popover.behavior = .transient
196-
197-
let controller = DatePickerController(date: nil, calendar: nil, style: .dateTime)
198-
controller.hasTextField = false
199-
controller.hasEdgePadding = true
200-
201-
popover.contentViewController = controller
186+
popover.contentViewController = datePickerControllers[.popover]
202187
popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .maxY)
203188
}
204189

205190
@objc func changeColor(_ sender: NSColorPanel?) {
206-
datePickerController?.customSelectionColor = sender?.color
207-
menuDatePickerController?.customSelectionColor = sender?.color
191+
datePickerControllers.values.forEach { $0.customSelectionColor = sender?.color }
208192
}
209193

210194
private let chineseLunarCalendar: Calendar = {
@@ -214,8 +198,12 @@ class TestDatePickerController: NSViewController {
214198
return calendar
215199
}()
216200

217-
private var datePickerController: DatePickerController?
218-
private var menuDatePickerController: DatePickerController?
201+
private enum DatePickerLocation: CaseIterable {
202+
case inline
203+
case menu
204+
case popover
205+
}
206+
private var datePickerControllers: [DatePickerLocation: DatePickerController] = [:]
219207
private var datePickerMenuItem: NSMenuItem?
220208

221209
private let delegateMessagesTextView: NSTextView = {

Sources/FluentUI_macOS/Components/DatePicker/CalendarHeaderView.swift

+67-25
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,66 @@ private struct Constants {
2828
private init() {}
2929
}
3030

31+
/// Allows the top row of the `CalendarHeaderView` to function as an accessible
32+
/// stepper for moving between months. Increment to move forwards, decrement to
33+
/// move backwards.
34+
class AccessibleCalendarHeaderStackView: NSStackView, NSAccessibilityStepper {
35+
init(monthYearLabel: NSTextField, leadingButton: NSButton, trailingButton: NSButton) {
36+
self.monthYearLabel = monthYearLabel
37+
self.leadingButton = leadingButton
38+
self.trailingButton = trailingButton
39+
40+
super.init(frame: .zero)
41+
42+
self.setAccessibilityElement(true)
43+
self.setAccessibilityRole(.incrementor)
44+
45+
self.addView(monthYearLabel, in: .center)
46+
self.addView(leadingButton, in: .leading)
47+
self.addView(trailingButton, in: .trailing)
48+
}
49+
50+
required init?(coder: NSCoder) {
51+
fatalError("init(coder:) has not been implemented")
52+
}
53+
54+
// MARK: NSAccessibilityStepper methods
55+
56+
override func accessibilityPerformIncrement() -> Bool {
57+
return performStep(trailingButton)
58+
}
59+
60+
override func accessibilityPerformDecrement() -> Bool {
61+
return performStep(leadingButton)
62+
}
63+
64+
override func accessibilityLabel() -> String? {
65+
return monthYearLabel.accessibilityLabel()
66+
}
67+
68+
override func accessibilityValue() -> Any? {
69+
return monthYearLabel.accessibilityValue()
70+
}
71+
72+
// MARK: Private
73+
74+
private func performStep(_ button: NSButton) -> Bool {
75+
let isEnabled = button.isEnabled
76+
if (isEnabled) {
77+
button.performClick(button)
78+
NSAccessibility.post(element: self, notification: .announcementRequested, userInfo: [
79+
NSAccessibility.NotificationUserInfoKey.announcement: self.accessibilityValue() ?? "",
80+
NSAccessibility.NotificationUserInfoKey.priority: NSAccessibilityPriorityLevel.medium.rawValue
81+
])
82+
}
83+
return isEnabled
84+
}
85+
86+
private let monthYearLabel: NSTextField
87+
private let leadingButton: NSButton
88+
private let trailingButton: NSButton
89+
}
90+
3191
/// Two-row calendar header that includes arrow buttons and the month-year label in the first row,
3292
/// and weekday column labels in the second row
3393
class CalendarHeaderView: NSView {
@@ -44,37 +104,19 @@ class CalendarHeaderView: NSView {
44104

45105
addSubview(containerStackView)
46106

47-
let headerStackView = NSStackView()
48-
headerStackView.translatesAutoresizingMaskIntoConstraints = false
49-
headerStackView.orientation = .horizontal
50-
headerStackView.distribution = .gravityAreas
51-
headerStackView.wantsLayer = true
52-
53-
containerStackView.addView(headerStackView, in: .top)
54-
55107
let leadingButton = NSButton(image: NSImage(named: NSImage.goBackTemplateName)!, target: self, action: #selector(leadingButtonPressed))
56108
let trailingButton = NSButton(image: NSImage(named: NSImage.goForwardTemplateName)!, target: self, action: #selector(trailingButtonPressed))
57109

58110
leadingButton.isBordered = false
59111
trailingButton.isBordered = false
60112

61-
leadingButton.setAccessibilityLabel(NSLocalizedString(
62-
"DATEPICKER_ACCESSIBILITY_PREVIOUS_MONTH_LABEL",
63-
tableName: "FluentUI",
64-
bundle: FluentUIResources.resourceBundle,
65-
comment: ""
66-
))
67-
68-
trailingButton.setAccessibilityLabel(NSLocalizedString(
69-
"DATEPICKER_ACCESSIBILITY_NEXT_MONTH_LABEL",
70-
tableName: "FluentUI",
71-
bundle: FluentUIResources.resourceBundle,
72-
comment: ""
73-
))
74-
75-
headerStackView.addView(monthYearLabel, in: .center)
76-
headerStackView.addView(leadingButton, in: .leading)
77-
headerStackView.addView(trailingButton, in: .trailing)
113+
let headerStackView = AccessibleCalendarHeaderStackView(monthYearLabel: monthYearLabel, leadingButton: leadingButton, trailingButton: trailingButton)
114+
headerStackView.translatesAutoresizingMaskIntoConstraints = false
115+
headerStackView.orientation = .horizontal
116+
headerStackView.distribution = .gravityAreas
117+
headerStackView.wantsLayer = true
118+
119+
containerStackView.addView(headerStackView, in: .top)
78120

79121
let weekdayLabelStackView = NSStackView()
80122
weekdayLabelStackView.translatesAutoresizingMaskIntoConstraints = false

0 commit comments

Comments
 (0)