From 6227cf9a8baa6adbd64b5c8b91f6c078006aa331 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 30 Jul 2025 16:07:55 -0500 Subject: [PATCH] Keyboard arrow through pins and notifications --- app/assets/images/pinned.svg | 2 +- app/assets/images/unpinned.svg | 2 +- app/assets/stylesheets/trays.css | 92 ++++++++++++++----- .../controllers/dialog_controller.js | 1 + .../controllers/navigable_list_controller.js | 13 ++- app/views/cards/assignments/new.html.erb | 2 +- .../cards/display/perma/_collection.html.erb | 2 +- app/views/cards/taggings/new.html.erb | 2 +- app/views/filters/_indexed_by.html.erb | 2 +- app/views/my/pins/_pin.html.erb | 4 +- app/views/my/pins/_tray.html.erb | 9 +- app/views/notifications/_tray.html.erb | 42 +++++---- 12 files changed, 118 insertions(+), 55 deletions(-) diff --git a/app/assets/images/pinned.svg b/app/assets/images/pinned.svg index 51c0e3292..c3d00761c 100644 --- a/app/assets/images/pinned.svg +++ b/app/assets/images/pinned.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/unpinned.svg b/app/assets/images/unpinned.svg index bada0866a..37313eb27 100644 --- a/app/assets/images/unpinned.svg +++ b/app/assets/images/unpinned.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 74e18c62e..a015dc696 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -76,6 +76,10 @@ display: none; } + .tray__toggle-text kbd { + vertical-align: text-bottom; + } + @media (max-width: 799px) { /* When collapsed on mobile, make it small */ .tray__dialog:not([open]) ~ & { @@ -141,21 +145,17 @@ position: relative; - &[aria-selected]{ - .card { - border-radius: 0.25ch; - outline: var(--focus-ring-size) solid var(--focus-ring-color); - outline-offset: var(--focus-ring-offset); - } - } - .tray__dialog & { font-size: 10px; margin-block-end: var(--tray-item-margin); transition: var(--tray-duration) var(--tray-timing-function); transition-delay: var(--tray-item-delay); transition-property: margin, scale; - z-index: var(--tray-item-z); + + + &:not(.tray__hat) { + z-index: var(--tray-item-z); + } &:has(*:focus-visible) { z-index: calc(var(--tray-item-z) + 1); @@ -230,6 +230,10 @@ font-size: 12px; } } + + .tray__dialog:not([open]) & { + pointer-events: none; + } } > *:is(:first-child, :last-child) { @@ -244,17 +248,18 @@ display: none; position: relative; - /* Red dot */ - &:after { - background-color: oklch(var(--lch-red-medium)); - block-size: 1ch; - border-radius: 50%; - box-shadow: 0 0 0 1px var(--tray-hat-bg); - content: ""; - inline-size: 1ch; - inset: 25% auto auto 50%; - position: absolute; - translate: 25% -75%; + .btn:has(&) { /* Red dot */ + &:after { + background-color: oklch(var(--lch-red-medium)); + block-size: 1ch; + border-radius: 50%; + box-shadow: 0 0 0 1px var(--tray-hat-bg); + content: ""; + inline-size: 1ch; + inset: 25% auto auto 50%; + position: absolute; + translate: 25% -75%; + } } } @@ -320,7 +325,24 @@ position: relative; + [open] &[aria-selected] { + outline: 0; + z-index: calc(var(--tray-item-z) + 2); + + .card__link { + border-radius: 0.25ch; + outline: var(--focus-ring-size) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + z-index: 1; + } + } + &:nth-child(1n + 11) { display: none; } + + &:not([aria-selected]) .card__link:focus-visible, + .tray__dialog:not([open]) & .card__link:focus-visible { + --focus-ring-size: 0; + } } .tray__remove-pin-btn { @@ -436,8 +458,34 @@ .tray--notifications { inset-inline: auto var(--tray-margin); - .tray__item:nth-child(1n + 7) { - display: none; + .tray__item, + [data-navigable-list-target~=item] { + [open] &[aria-selected] { + outline: 0; + z-index: calc(var(--tray-item-z) + 2); + + .card, + .tray__hat & .btn { + border-radius: 0.25ch; + outline: var(--focus-ring-size) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + } + } + + &:nth-child(1n + 7) { + display: none; + pointer-events: none; + visibility: hidden; + } + + &:not([aria-selected]) .card:focus-visible, + .tray__dialog:not([open]) & .card:focus-visible { + --focus-ring-size: 0; + } + + .btn:focus-visible { + outline: 0; + } } &:not(:has(.card--notification)) { diff --git a/app/javascript/controllers/dialog_controller.js b/app/javascript/controllers/dialog_controller.js index 123c2384e..b1d9eee4a 100644 --- a/app/javascript/controllers/dialog_controller.js +++ b/app/javascript/controllers/dialog_controller.js @@ -19,6 +19,7 @@ export default class extends Controller { this.dialogTarget.show() } this.dialogTarget.setAttribute('aria-hidden', 'false') + this.dispatch("show") } toggle() { diff --git a/app/javascript/controllers/navigable_list_controller.js b/app/javascript/controllers/navigable_list_controller.js index 6347e9fde..580dafc6c 100644 --- a/app/javascript/controllers/navigable_list_controller.js +++ b/app/javascript/controllers/navigable_list_controller.js @@ -7,7 +7,8 @@ export default class extends Controller { reverseOrder: { type: Boolean, default: false }, selectionAttribute: { type: String, default: "aria-selected" }, focusOnSelection: { type: Boolean, default: true }, - actionableItems: { type: Boolean, default: false } + actionableItems: { type: Boolean, default: false }, + reverseNavigation: { type: Boolean, default: false } } connect() { @@ -51,7 +52,9 @@ export default class extends Controller { // Private get #visibleItems() { - return this.itemTargets.filter(item => !item.hidden) + return this.itemTargets.filter(item => { + return item.checkVisibility() && !item.hidden + }) } #selectPrevious() { @@ -113,10 +116,12 @@ export default class extends Controller { #keyHandlers = { ArrowDown(event) { - this.#handleArrowKey(event, this.#selectNext.bind(this)) + const selectMethod = this.reverseNavigationValue ? this.#selectPrevious.bind(this) : this.#selectNext.bind(this) + this.#handleArrowKey(event, selectMethod) }, ArrowUp(event) { - this.#handleArrowKey(event, this.#selectPrevious.bind(this)) + const selectMethod = this.reverseNavigationValue ? this.#selectNext.bind(this) : this.#selectPrevious.bind(this) + this.#handleArrowKey(event, selectMethod) }, ArrowRight(event) { this.#handleArrowKey(event, this.#selectNext.bind(this), false) diff --git a/app/views/cards/assignments/new.html.erb b/app/views/cards/assignments/new.html.erb index f0d557607..69c499524 100644 --- a/app/views/cards/assignments/new.html.erb +++ b/app/views/cards/assignments/new.html.erb @@ -1,6 +1,6 @@ <%= turbo_frame_tag @card, :assignment do %> <%= tag.div class: "max-width", data: { - action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate filter:changed->navigable-list#reset", + action: "turbo:before-cache@document->dialog#close dialog:show@document->navigable-list#reset keydown->navigable-list#navigate filter:changed->navigable-list#reset", controller: "filter navigable-list", dialog_target: "dialog", navigable_list_focus_on_selection_value: false, diff --git a/app/views/cards/display/perma/_collection.html.erb b/app/views/cards/display/perma/_collection.html.erb index 72216b8b3..250f245ca 100644 --- a/app/views/cards/display/perma/_collection.html.erb +++ b/app/views/cards/display/perma/_collection.html.erb @@ -7,7 +7,7 @@ <%= tag.div class: "max-width full-width", data: { - action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate filter:changed->navigable-list#reset", + action: "turbo:before-cache@document->dialog#close dialog:show@document->navigable-list#reset keydown->navigable-list#navigate filter:changed->navigable-list#reset", controller: "filter navigable-list", dialog_target: "dialog", navigable_list_focus_on_selection_value: false, diff --git a/app/views/cards/taggings/new.html.erb b/app/views/cards/taggings/new.html.erb index 6f7b1ec1a..48876fc29 100644 --- a/app/views/cards/taggings/new.html.erb +++ b/app/views/cards/taggings/new.html.erb @@ -3,7 +3,7 @@ data-controller="filter navigable-list" data-navigable-list-focus-on-selection-value="false" data-navigable-list-actionable-items-value="true" - data-action="keydown->navigable-list#navigate filter:changed->navigable-list#reset" + data-action="keydown->navigable-list#navigate filter:changed->navigable-list#reset dialog:show@document->navigable-list#reset" > Tag this… diff --git a/app/views/filters/_indexed_by.html.erb b/app/views/filters/_indexed_by.html.erb index bf0480d64..2a2f0756e 100644 --- a/app/views/filters/_indexed_by.html.erb +++ b/app/views/filters/_indexed_by.html.erb @@ -7,7 +7,7 @@ <%= tag.dialog class: "margin-block-start-half popup panel flex-column align-start gap-half fill-white shadow txt-small", data: { - action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate", + action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate dialog:show@document->navigable-list#reset", aria: { label: "Sort by…", aria_description: "Sort by…" }, controller: "navigable-list", dialog_target: "dialog", diff --git a/app/views/my/pins/_pin.html.erb b/app/views/my/pins/_pin.html.erb index 5d80b4bbe..68a6899f4 100644 --- a/app/views/my/pins/_pin.html.erb +++ b/app/views/my/pins/_pin.html.erb @@ -1,6 +1,6 @@ -
+
<%= render "cards/display/preview", card: pin.card %> <%= button_to card_pin_path(pin.card), method: :delete, class: "tray__remove-pin-btn btn btn--circle borderless" do %> - <%= icon_tag "remove-med" %> Un-pin this card + <%= icon_tag "pinned" %> Un-pin this card <% end %>
diff --git a/app/views/my/pins/_tray.html.erb b/app/views/my/pins/_tray.html.erb index 0db8d9295..8114454bd 100644 --- a/app/views/my/pins/_tray.html.erb +++ b/app/views/my/pins/_tray.html.erb @@ -1,8 +1,13 @@ <%= turbo_stream_from Current.user, :pins_tray %>
- <%= tag.dialog id: "pin-tray", class: "tray__dialog", - data: { turbo_permanent: true, dialog_modal_value: false, dialog_target: "dialog", action: "keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside" } do %> + <%= tag.dialog id: "pin-tray", class: "tray__dialog", data: { + action: "keydown->navigable-list#navigate dialog:show@document->navigable-list#reset keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside", + controller: "navigable-list", + dialog_target: "dialog", + navigable_list_actionable_items_value: "true", + navigable_list_reverse_navigation_value: "true" }, + turbo_permanent: true do %> <%= turbo_frame_tag "pins", src: my_pins_path %> <% end %> diff --git a/app/views/notifications/_tray.html.erb b/app/views/notifications/_tray.html.erb index 3eadde953..585196dbe 100644 --- a/app/views/notifications/_tray.html.erb +++ b/app/views/notifications/_tray.html.erb @@ -2,33 +2,37 @@
+ data-controller="navigable-list" + data-dialog-target="dialog" + data-navigable-list-actionable-items-value="true" + data-navigable-list-reverse-navigation-value="true" + data-action="keydown->navigable-list#navigate dialog:show@document->navigable-list#reset turbo-visit->navigable-list#reset keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside"> <%= turbo_frame_tag "notifications", src: tray_notifications_path, refresh: "morph" %>
- <%= link_to settings_notifications_path, - class: "btn borderless tray__notification-settings", - title: "Notification settings", - data: { action: "dialog#close" } do %> - <%= icon_tag "settings" %> - Settings - <% end %> +
+ <%= link_to settings_notifications_path, + class: "btn borderless tray__notification-settings", + title: "Notification settings", + data: { action: "dialog#close" } do %> + <%= icon_tag "settings" %> + Settings + <% end %> +
- <%= link_to notifications_path, class: "tray__new-notifications btn borderless flex-item-grow position-relative overflow-ellipsis", data: { action: "click->dialog#close" } do %> - <%= icon_tag "bell" %> - See more new items - <% end %> - - <%= link_to notifications_path, class: "tray__old-notifications btn borderless flex-item-grow position-relative overflow-ellipsis", data: { action: "click->dialog#close" } do %> - <%= icon_tag "bell" %> - See older items - <% end %> +
+ <%= link_to notifications_path, class: "btn borderless flex-item-grow position-relative overflow-ellipsis", data: { action: "click->dialog#close" } do %> + <%= icon_tag "bell" %> + See more new items + See older items + <% end %> +
<%= button_to read_all_notifications_path, class: "btn borderless tray__clear-notifications", title: "Mark all notifications as read", - data: { action: "dialog#close badge#clear", turbo_frame: "notifications" } do %> + data: { action: "dialog#close badge#clear", turbo_frame: "notifications" }, + form: { data: { navigable_list_target: "item" } } do %> <%= icon_tag "check" %> Clear all <% end %>