Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[macOS] Announce DatePickerController changes in VoiceOver #2134

Merged
merged 6 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ class TestDatePickerController: NSViewController {
let calendar = Calendar.current
let date = calendar.date(from: DateComponents(year: 2019, month: 12, day: 9))

datePickerController = DatePickerController(date: date, calendar: calendar, style: .dateTime)
menuDatePickerController = DatePickerController(date: date, calendar: calendar, style: .dateTime)

datePickerController?.delegate = self
menuDatePickerController?.delegate = self

datePickerController?.hasEdgePadding = true
menuDatePickerController?.hasEdgePadding = true
for location in DatePickerLocation.allCases {
let datePickerController = DatePickerController(date: date, calendar: calendar, style: .dateTime)
datePickerController.delegate = self
datePickerController.hasEdgePadding = true
datePickerControllers[location] = datePickerController
}
}

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

horizontalStack.addView(vfxView, in: .center)
if let controller = datePickerController {
if let controller = datePickerControllers[.inline] {
vfxView.addSubview(controller.view)

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

if let controller = menuDatePickerController, let menuItem = datePickerMenuItem {
if let controller = datePickerControllers[.menu], let menuItem = datePickerMenuItem {
menuItem.view = NSView(frame: NSRect(origin: .zero, size: controller.view.fittingSize))
menuItem.view?.addSubview(controller.view)
menu.addItem(menuItem)
Expand Down Expand Up @@ -128,8 +126,8 @@ class TestDatePickerController: NSViewController {
let delegateMessagesScrollView = NSScrollView()
delegateMessagesScrollView.documentView = delegateMessagesTextView

[delegateMessagesLabel, delegateMessagesScrollView].forEach {
containerView.addView($0, in: .bottom)
for view in [delegateMessagesLabel, delegateMessagesScrollView] {
containerView.addView(view, in: .bottom)
}

NSLayoutConstraint.activate([
Expand All @@ -145,8 +143,7 @@ class TestDatePickerController: NSViewController {
}

@objc func clearCustomColor() {
datePickerController?.customSelectionColor = nil
menuDatePickerController?.customSelectionColor = nil
datePickerControllers.values.forEach { $0.customSelectionColor = nil }
}

@objc func launchColorPicker() {
Expand All @@ -157,54 +154,41 @@ class TestDatePickerController: NSViewController {
}

@objc func toggleSecondaryCalendar() {
if datePickerController?.secondaryCalendar == nil {
datePickerController?.secondaryCalendar = chineseLunarCalendar
} else {
datePickerController?.secondaryCalendar = nil
}

if menuDatePickerController?.secondaryCalendar == nil {
menuDatePickerController?.secondaryCalendar = chineseLunarCalendar
} else {
menuDatePickerController?.secondaryCalendar = nil
for datePickerController in datePickerControllers.values {
if datePickerController.secondaryCalendar == nil {
datePickerController.secondaryCalendar = chineseLunarCalendar
} else {
datePickerController.secondaryCalendar = nil
}
}
}

@objc func toggleAutoSelection(_ sender: NSButton) {
let enabled = sender.state == .on
datePickerController?.autoSelectWhenPaging = enabled
menuDatePickerController?.autoSelectWhenPaging = enabled
datePickerControllers.values.forEach { $0.autoSelectWhenPaging = enabled }
}

@objc func toggleEdgePadding(_ sender: NSButton) {
let enabled = sender.state == .on
datePickerController?.hasEdgePadding = enabled
menuDatePickerController?.hasEdgePadding = enabled
datePickerMenuItem?.view?.frame.size = menuDatePickerController?.view.fittingSize ?? .zero
datePickerControllers.values.forEach { $0.hasEdgePadding = enabled }
datePickerMenuItem?.view?.frame.size = datePickerControllers[.menu]?.view.fittingSize ?? .zero
}

@objc func toggleTextDatePicker(_ sender: NSButton) {
let enabled = sender.state == .on
datePickerController?.hasTextField = enabled
menuDatePickerController?.hasTextField = enabled
datePickerMenuItem?.view?.frame.size = menuDatePickerController?.view.fittingSize ?? .zero
datePickerControllers.values.forEach { $0.hasTextField = enabled }
datePickerMenuItem?.view?.frame.size = datePickerControllers[.menu]?.view.fittingSize ?? .zero
}

@objc func showPopover(_ sender: NSButton) {
let popover = NSPopover()
popover.behavior = .transient

let controller = DatePickerController(date: nil, calendar: nil, style: .dateTime)
controller.hasTextField = false
controller.hasEdgePadding = true

popover.contentViewController = controller
popover.contentViewController = datePickerControllers[.popover]
popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .maxY)
}

@objc func changeColor(_ sender: NSColorPanel?) {
datePickerController?.customSelectionColor = sender?.color
menuDatePickerController?.customSelectionColor = sender?.color
datePickerControllers.values.forEach { $0.customSelectionColor = sender?.color }
}

private let chineseLunarCalendar: Calendar = {
Expand All @@ -214,8 +198,12 @@ class TestDatePickerController: NSViewController {
return calendar
}()

private var datePickerController: DatePickerController?
private var menuDatePickerController: DatePickerController?
private enum DatePickerLocation: CaseIterable {
case inline
case menu
case popover
}
private var datePickerControllers: [DatePickerLocation: DatePickerController] = [:]
private var datePickerMenuItem: NSMenuItem?

private let delegateMessagesTextView: NSTextView = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,66 @@ private struct Constants {
private init() {}
}

/// Allows the top row of the `CalendarHeaderView` to function as an accessible
/// stepper for moving between months. Increment to move forwards, decrement to
/// move backwards.
class AccessibleCalendarHeaderStackView: NSStackView, NSAccessibilityStepper {
init(monthYearLabel: NSTextField, leadingButton: NSButton, trailingButton: NSButton) {
self.monthYearLabel = monthYearLabel
self.leadingButton = leadingButton
self.trailingButton = trailingButton

super.init(frame: .zero)

self.setAccessibilityElement(true)
self.setAccessibilityRole(.incrementor)

self.addView(monthYearLabel, in: .center)
self.addView(leadingButton, in: .leading)
self.addView(trailingButton, in: .trailing)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: NSAccessibilityStepper methods

override func accessibilityPerformIncrement() -> Bool {
return performStep(trailingButton)
}

override func accessibilityPerformDecrement() -> Bool {
return performStep(leadingButton)
}

override func accessibilityLabel() -> String? {
return monthYearLabel.accessibilityLabel()
}

override func accessibilityValue() -> Any? {
return monthYearLabel.accessibilityValue()
}

// MARK: Private

private func performStep(_ button: NSButton) -> Bool {
let isEnabled = button.isEnabled
if (isEnabled) {
button.performClick(button)
NSAccessibility.post(element: self, notification: .announcementRequested, userInfo: [
NSAccessibility.NotificationUserInfoKey.announcement: self.accessibilityValue() ?? "",
NSAccessibility.NotificationUserInfoKey.priority: NSAccessibilityPriorityLevel.medium.rawValue
])
}
return isEnabled
}

private let monthYearLabel: NSTextField
private let leadingButton: NSButton
private let trailingButton: NSButton
}

/// Two-row calendar header that includes arrow buttons and the month-year label in the first row,
/// and weekday column labels in the second row
class CalendarHeaderView: NSView {
Expand All @@ -44,37 +104,19 @@ class CalendarHeaderView: NSView {

addSubview(containerStackView)

let headerStackView = NSStackView()
headerStackView.translatesAutoresizingMaskIntoConstraints = false
headerStackView.orientation = .horizontal
headerStackView.distribution = .gravityAreas
headerStackView.wantsLayer = true

containerStackView.addView(headerStackView, in: .top)

let leadingButton = NSButton(image: NSImage(named: NSImage.goBackTemplateName)!, target: self, action: #selector(leadingButtonPressed))
let trailingButton = NSButton(image: NSImage(named: NSImage.goForwardTemplateName)!, target: self, action: #selector(trailingButtonPressed))

leadingButton.isBordered = false
trailingButton.isBordered = false

leadingButton.setAccessibilityLabel(NSLocalizedString(
"DATEPICKER_ACCESSIBILITY_PREVIOUS_MONTH_LABEL",
tableName: "FluentUI",
bundle: FluentUIResources.resourceBundle,
comment: ""
))

trailingButton.setAccessibilityLabel(NSLocalizedString(
"DATEPICKER_ACCESSIBILITY_NEXT_MONTH_LABEL",
tableName: "FluentUI",
bundle: FluentUIResources.resourceBundle,
comment: ""
))

headerStackView.addView(monthYearLabel, in: .center)
headerStackView.addView(leadingButton, in: .leading)
headerStackView.addView(trailingButton, in: .trailing)
let headerStackView = AccessibleCalendarHeaderStackView(monthYearLabel: monthYearLabel, leadingButton: leadingButton, trailingButton: trailingButton)
headerStackView.translatesAutoresizingMaskIntoConstraints = false
headerStackView.orientation = .horizontal
headerStackView.distribution = .gravityAreas
headerStackView.wantsLayer = true

containerStackView.addView(headerStackView, in: .top)

let weekdayLabelStackView = NSStackView()
weekdayLabelStackView.translatesAutoresizingMaskIntoConstraints = false
Expand Down
Loading