fix: keep emoji selection visible during keyboard navigation
This commit is contained in:
@@ -95,7 +95,7 @@ Rates are shared across queries and refreshed on the next currency query after t
|
|||||||
|
|
||||||
Search for **Conjure Emoji**, identified by its centered 🤔 icon, and open it. Browse the grid or search by name, keyword, `:shortcode:`, or emoji. Use the category menu to filter results or show **Recently Used**. Your last 30 selections are saved locally. Favorites appear first, followed by recent selections when browsing All Emoji.
|
Search for **Conjure Emoji**, identified by its centered 🤔 icon, and open it. Browse the grid or search by name, keyword, `:shortcode:`, or emoji. Use the category menu to filter results or show **Recently Used**. Your last 30 selections are saved locally. Favorites appear first, followed by recent selections when browsing All Emoji.
|
||||||
|
|
||||||
Use the arrow keys or Control-H/J/K/L to navigate left/down/up/right. Return, clicking an emoji, or **Copy and Paste** copies it, dismisses Conjure, and tries to paste into the app that was focused when the launcher opened. Automatic pasting requires **System Settings → Privacy & Security → Accessibility → Conjure**; macOS prompts if permission is missing. After granting it, select an emoji again. If permission is denied, the previous app is unavailable, or focus cannot be restored promptly, the emoji stays copied for manual Command-V. Escape or the Back chevron returns to the launcher and restores your search.
|
Use the arrow keys or Control-H/J/K/L to navigate left/down/up/right; the grid scrolls to keep the selected emoji visible. Return, clicking an emoji, or **Copy and Paste** copies it, dismisses Conjure, and tries to paste into the app that was focused when the launcher opened. Automatic pasting requires **System Settings → Privacy & Security → Accessibility → Conjure**; macOS prompts if permission is missing. After granting it, select an emoji again. If permission is denied, the previous app is unavailable, or focus cannot be restored promptly, the emoji stays copied for manual Command-V. Escape or the Back chevron returns to the launcher and restores your search.
|
||||||
|
|
||||||
Open **Actions** with Command-K for **Copy and Paste** (default), **Copy Emoji** (copy only, no Accessibility permission needed), **Favorite**, or **Remove Favorite**. Favorites are saved locally and grouped ahead of other emoji, with a small extra spacing break when browsing without a search. There are no star badges.
|
Open **Actions** with Command-K for **Copy and Paste** (default), **Copy Emoji** (copy only, no Accessibility permission needed), **Favorite**, or **Remove Favorite**. Favorites are saved locally and grouped ahead of other emoji, with a small extra spacing break when browsing without a search. There are no star badges.
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
|||||||
selected = min(max(selected + offset, 0), results.count - 1)
|
selected = min(max(selected + offset, 0), results.count - 1)
|
||||||
(collection.item(at: indexPath(for: old)) as? EmojiItem)?.highlight(false)
|
(collection.item(at: indexPath(for: old)) as? EmojiItem)?.highlight(false)
|
||||||
updateSelection()
|
updateSelection()
|
||||||
collection.scrollToItems(at: [indexPath(for: selected)], scrollPosition: .nearestVerticalEdge)
|
collection.scrollToItems(at: [indexPath(for: selected)], scrollPosition: .nearestHorizontalEdge)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var columns: Int { max(1, Int((collection.bounds.width - 32 + 6) / 70)) }
|
private var columns: Int { max(1, Int((collection.bounds.width - 32 + 6) / 70)) }
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
- [x] tighten Command presentation lifetime contracts: guarantee complete Command-owned Actions teardown and distinguish one-level Back from full dismissal or destination replacement. Fixed in the existing controllers without extracting another presentation module. Covered nested Escape, query restoration, replacement with Actions open, and copy/paste dismissal.
|
- [x] bugfix: the emoji picker currently doesn't scroll with the selection (arrows or ctrl+hjkl), only with mouse scroll.
|
||||||
|
- the window should scroll with the current selection
|
||||||
|
- [ ] make escape close the window that was opened, be it the application launcher, the emoji picker, or something else.
|
||||||
|
- clicking the "back" chevron should still take you back to the application launcher view though (as it does today).
|
||||||
- [ ] add a notes app inspired by raycast notes
|
- [ ] add a notes app inspired by raycast notes
|
||||||
- [ ] improve on the conjure translation command
|
- [ ] improve on the conjure translation command
|
||||||
@@ -51,6 +51,53 @@ import Testing
|
|||||||
#expect(defaults.stringArray(forKey: "recentEmoji") == ["👩💻", matches[1].value])
|
#expect(defaults.stringArray(forKey: "recentEmoji") == ["👩💻", matches[1].value])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(arguments: [false, true], [false, true])
|
||||||
|
@MainActor func emojiKeyboardNavigationKeepsSelectionVisible(controlKeys: Bool, favorites: Bool) throws {
|
||||||
|
_ = NSApplication.shared
|
||||||
|
let suite = "ConjureEmojiScroll.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
|
let pasteboard = NSPasteboard.withUniqueName()
|
||||||
|
defer { defaults.removePersistentDomain(forName: suite); pasteboard.releaseGlobally() }
|
||||||
|
let favored = favorites ? Array(EmojiCatalog.all.suffix(3)) : []
|
||||||
|
defaults.set(favored.map(\.value), forKey: "favoriteEmoji")
|
||||||
|
let ordered = favored + EmojiCatalog.all.filter { emoji in !favored.contains { $0.value == emoji.value } }
|
||||||
|
let picker = EmojiPickerController(defaults: defaults, pasteboard: pasteboard)
|
||||||
|
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 400),
|
||||||
|
styleMask: [.borderless], backing: .buffered, defer: false)
|
||||||
|
window.contentView = picker.view
|
||||||
|
picker.view.layoutSubtreeIfNeeded()
|
||||||
|
let scroll = try #require(picker.view.subviews.compactMap { $0 as? NSScrollView }.first)
|
||||||
|
let grid = try #require(scroll.documentView as? NSCollectionView)
|
||||||
|
func expectVisible() throws {
|
||||||
|
picker.copySelection()
|
||||||
|
let index = try #require(ordered.firstIndex { $0.value == pasteboard.string(forType: .string) })
|
||||||
|
let path = favorites && index >= favored.count
|
||||||
|
? IndexPath(item: index - favored.count, section: 1) : IndexPath(item: index, section: 0)
|
||||||
|
let frame = try #require(grid.collectionViewLayout?.layoutAttributesForItem(at: path)?.frame)
|
||||||
|
try #require(scroll.documentVisibleRect.contains(frame))
|
||||||
|
}
|
||||||
|
for down in [true, false] {
|
||||||
|
for _ in 0..<12 {
|
||||||
|
if controlKeys {
|
||||||
|
#expect(picker.handleControlKey(down ? "j" : "k"))
|
||||||
|
} else {
|
||||||
|
#expect(picker.handle(down ? #selector(NSResponder.moveDown(_:)) : #selector(NSResponder.moveUp(_:))))
|
||||||
|
}
|
||||||
|
try expectVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for right in [true, false] {
|
||||||
|
for _ in 0..<80 {
|
||||||
|
if controlKeys {
|
||||||
|
#expect(picker.handleControlKey(right ? "l" : "h"))
|
||||||
|
} else {
|
||||||
|
#expect(picker.handle(right ? #selector(NSResponder.moveRight(_:)) : #selector(NSResponder.moveLeft(_:))))
|
||||||
|
}
|
||||||
|
try expectVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test @MainActor func emojiActionsDefaultToPasteAndKeepCopyOnlyAvailable() throws {
|
@Test @MainActor func emojiActionsDefaultToPasteAndKeepCopyOnlyAvailable() throws {
|
||||||
_ = NSApplication.shared
|
_ = NSApplication.shared
|
||||||
let suite = "ConjureEmojiActions.\(UUID().uuidString)"
|
let suite = "ConjureEmojiActions.\(UUID().uuidString)"
|
||||||
|
|||||||
Reference in New Issue
Block a user