-
Notifications
You must be signed in to change notification settings - Fork 131
[PM-38360] feat: Add reusable DateFieldPicker component #2731
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
Draft
SaintPatrck
wants to merge
7
commits into
main
Choose a base branch
from
vault/pm-38360-ios-date-field-picker
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
1873825
[PM-38360] feat: Add reusable DateFieldPicker component
SaintPatrck b8137a4
Present the date picker as a popover dialog
SaintPatrck 34c160f
Use a date-only wheel picker and fix the clipped Clear button
SaintPatrck 363cbc2
Switch to inline graphical calendar picker with 24pt clear icon
SaintPatrck a0bfdce
Remove date field empty-state placeholder to match design comps
SaintPatrck 1627230
Improve DateFieldPicker accessibility
SaintPatrck a8048db
Add DateFieldPicker showcase to Test Harness
SaintPatrck File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import BitwardenKit | ||
| import XCTest | ||
|
|
||
| class DateTests: BitwardenTestCase { | ||
| // MARK: Tests | ||
|
|
||
| /// `iso8601DateOnlyString` formats a date as a `yyyy-MM-dd` calendar-date string. | ||
| func test_iso8601DateOnlyString() { | ||
| let date = Date(year: 2023, month: 6, day: 23) | ||
| XCTAssertEqual(date.iso8601DateOnlyString, "2023-06-23") | ||
| } | ||
|
|
||
| /// `iso8601DateOnlyString` zero-pads single-digit months and days. | ||
| func test_iso8601DateOnlyString_zeroPadded() { | ||
| let date = Date(year: 2024, month: 1, day: 5) | ||
| XCTAssertEqual(date.iso8601DateOnlyString, "2024-01-05") | ||
| } | ||
|
|
||
| /// `init(iso8601DateOnlyString:)` parses a valid calendar-date string into a date. | ||
| func test_initISO8601DateOnlyString_valid() { | ||
| let date = Date(iso8601DateOnlyString: "2023-06-23") | ||
| XCTAssertEqual(date, Date(year: 2023, month: 6, day: 23)) | ||
| } | ||
|
|
||
| /// `init(iso8601DateOnlyString:)` round-trips with `iso8601DateOnlyString`. | ||
| func test_initISO8601DateOnlyString_roundTrip() { | ||
| let original = Date(year: 1999, month: 12, day: 31) | ||
| let parsed = Date(iso8601DateOnlyString: original.iso8601DateOnlyString) | ||
| XCTAssertEqual(parsed, original) | ||
| } | ||
|
|
||
| /// `init(iso8601DateOnlyString:)` returns `nil` for an impossible calendar date. | ||
| func test_initISO8601DateOnlyString_invalidDate() { | ||
| XCTAssertNil(Date(iso8601DateOnlyString: "2023-02-30")) | ||
| } | ||
|
|
||
| /// `init(iso8601DateOnlyString:)` returns `nil` for a malformed string. | ||
| func test_initISO8601DateOnlyString_malformed() { | ||
| XCTAssertNil(Date(iso8601DateOnlyString: "")) | ||
| XCTAssertNil(Date(iso8601DateOnlyString: "not-a-date")) | ||
| XCTAssertNil(Date(iso8601DateOnlyString: "06/23/2023")) | ||
| } | ||
| } |
70 changes: 70 additions & 0 deletions
70
BitwardenKit/UI/Platform/Application/Views/DateFieldPicker+SnapshotTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // swiftlint:disable:this file_name | ||
| import SnapshotTesting | ||
| import SwiftUI | ||
| import XCTest | ||
|
|
||
| @testable import BitwardenKit | ||
|
|
||
| class DateFieldPickerSnapshotTests: BitwardenTestCase { | ||
| // MARK: Properties | ||
|
|
||
| /// A fixed date used so snapshots are deterministic. | ||
| let date = Date(year: 2023, month: 6, day: 23) | ||
|
|
||
| var subject: DateFieldPicker! | ||
|
|
||
| // MARK: Setup & Teardown | ||
|
|
||
| override func tearDown() { | ||
| super.tearDown() | ||
| subject = nil | ||
| } | ||
|
|
||
| // MARK: Tests | ||
|
|
||
| /// Test a snapshot of the collapsed empty field in light mode. | ||
| func disabletest_snapshot_collapsedEmpty_lightMode() { | ||
| subject = DateFieldPicker(title: "Date of birth", date: .constant(nil)) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortrait) | ||
| } | ||
|
|
||
| /// Test a snapshot of the collapsed empty field in dark mode. | ||
| func disabletest_snapshot_collapsedEmpty_darkMode() { | ||
| subject = DateFieldPicker(title: "Date of birth", date: .constant(nil)) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortraitDark) | ||
| } | ||
|
|
||
| /// Test a snapshot of the collapsed empty field with large dynamic type. | ||
| func disabletest_snapshot_collapsedEmpty_largeDynamicType() { | ||
| subject = DateFieldPicker(title: "Date of birth", date: .constant(nil)) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortraitAX5) | ||
| } | ||
|
|
||
| /// Test a snapshot of a collapsed field with a selected date in light mode. | ||
| func disabletest_snapshot_collapsedSelected_lightMode() { | ||
| subject = DateFieldPicker(title: "Expiration date", date: .constant(date)) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortrait) | ||
| } | ||
|
|
||
| /// Test a snapshot of a collapsed field with a selected date in dark mode. | ||
| func disabletest_snapshot_collapsedSelected_darkMode() { | ||
| subject = DateFieldPicker(title: "Expiration date", date: .constant(date)) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortraitDark) | ||
| } | ||
|
|
||
| /// Test a snapshot of a collapsed field with a footer in light mode. | ||
| func disabletest_snapshot_withFooter_lightMode() { | ||
| subject = DateFieldPicker( | ||
| title: "Expiration date", | ||
| date: .constant(date), | ||
| footer: "The date this document expires.", | ||
| ) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortrait) | ||
| } | ||
| } |
114 changes: 114 additions & 0 deletions
114
BitwardenKit/UI/Platform/Application/Views/DateFieldPicker+ViewInspectorTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| // swiftlint:disable:this file_name | ||
| import BitwardenKit | ||
| import BitwardenResources | ||
| import SwiftUI | ||
| import ViewInspector | ||
| import XCTest | ||
|
|
||
| class DateFieldPickerTests: BitwardenTestCase { | ||
| // MARK: Properties | ||
|
|
||
| /// The default date used to seed the picker when a date is first selected. | ||
| let defaultDate = Date(year: 2023, month: 6, day: 23) | ||
|
|
||
| /// The backing value for the field's date binding. | ||
| var date: Date? | ||
|
|
||
| var subject: DateFieldPicker! | ||
|
|
||
| /// A binding to the test's backing `date` value. | ||
| private var bindingDate: Binding<Date?> { | ||
| Binding { | ||
| self.date | ||
| } set: { newValue in | ||
| self.date = newValue | ||
| } | ||
| } | ||
|
|
||
| // MARK: Setup & Teardown | ||
|
|
||
| override func setUp() { | ||
| super.setUp() | ||
| date = nil | ||
| subject = DateFieldPicker( | ||
| title: "Date of birth", | ||
| date: bindingDate, | ||
| defaultDate: defaultDate, | ||
| ) | ||
| } | ||
|
|
||
| override func tearDown() { | ||
| super.tearDown() | ||
| date = nil | ||
| subject = nil | ||
| } | ||
|
|
||
| // MARK: Tests | ||
|
|
||
| /// When collapsed and empty, the field shows its title and no inline date picker. | ||
| func test_collapsedEmpty_showsTitleAndNoPicker() throws { | ||
| XCTAssertNoThrow(try subject.inspect().find(text: "Date of birth")) | ||
| XCTAssertThrowsError(try subject.inspect().find(ViewType.DatePicker.self)) | ||
| } | ||
|
|
||
| /// When a date is selected, the collapsed field shows the formatted date. | ||
| func test_collapsedSelected_showsFormattedDate() throws { | ||
| date = defaultDate | ||
| let expected = defaultDate.formatted(date: .long, time: .omitted) | ||
| XCTAssertNoThrow(try subject.inspect().find(text: expected)) | ||
| } | ||
|
|
||
| /// The collapsed header is a button so a single tap expands the picker. | ||
| func test_headerButton_exists() throws { | ||
| XCTAssertNoThrow(try subject.inspect().find(viewWithAccessibilityIdentifier: "DateFieldHeaderButton")) | ||
| } | ||
|
|
||
| /// The header button carries an accessibility hint telling VoiceOver users it selects a date. | ||
| func test_headerButton_hasSelectDateHint() throws { | ||
| let header = try subject.inspect().find(viewWithAccessibilityIdentifier: "DateFieldHeaderButton") | ||
| XCTAssertEqual(try header.accessibilityHint().string(), Localizations.selectDate) | ||
| } | ||
|
|
||
| /// The clear control's accessibility label names the field so VoiceOver users know what it clears. | ||
| func test_clearButton_accessibilityLabel_namesField() throws { | ||
| date = defaultDate | ||
| XCTAssertNoThrow( | ||
| try subject.inspect().find(viewWithAccessibilityLabel: Localizations.clearFieldName("Date of birth")), | ||
| ) | ||
| } | ||
|
|
||
| /// When a date is selected, a clear control is shown and tapping it resets the value to `nil`. | ||
| func test_clearButton_clearsDate() throws { | ||
| date = defaultDate | ||
| let clearButton = try subject.inspect().find(viewWithAccessibilityIdentifier: "DateFieldClearButton") | ||
| try clearButton.button().tap() | ||
| XCTAssertNil(date) | ||
| } | ||
|
|
||
| /// No clear control is shown when the field is empty. | ||
| func test_clearButton_hiddenWhenEmpty() throws { | ||
| XCTAssertThrowsError(try subject.inspect().find(viewWithAccessibilityIdentifier: "DateFieldClearButton")) | ||
| } | ||
|
|
||
| /// A provided footer is rendered below the field. | ||
| func test_footer_isRendered() throws { | ||
| subject = DateFieldPicker( | ||
| title: "Date of birth", | ||
| date: bindingDate, | ||
| defaultDate: defaultDate, | ||
| footer: "A footer", | ||
| ) | ||
| XCTAssertNoThrow(try subject.inspect().find(text: "A footer")) | ||
| } | ||
|
|
||
| /// The field applies the provided accessibility identifier. | ||
| func test_accessibilityIdentifier_custom() throws { | ||
| subject = DateFieldPicker( | ||
| title: "Date of birth", | ||
| accessibilityIdentifier: "DateOfBirthField", | ||
| date: bindingDate, | ||
| defaultDate: defaultDate, | ||
| ) | ||
| XCTAssertNoThrow(try subject.inspect().find(viewWithAccessibilityIdentifier: "DateOfBirthField")) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π€ How is this different from the built-in
ISO8601DateFormatter?