summaryrefslogtreecommitdiff
path: root/Omni
diff options
context:
space:
mode:
Diffstat (limited to 'Omni')
-rw-r--r--Omni/Jr/Web.hs104
-rw-r--r--Omni/Jr/Web/Style.hs7
2 files changed, 102 insertions, 9 deletions
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index c7e2e44..9da31c9 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -187,6 +187,78 @@ pageHead title =
Lucid.crossorigin_ "anonymous"
]
("" :: Text)
+ Lucid.script_ [] statusDropdownJs
+
+statusDropdownJs :: Text
+statusDropdownJs =
+ Text.unlines
+ [ "function toggleStatusDropdown(el) {",
+ " var container = el.parentElement;",
+ " var isOpen = container.classList.toggle('open');",
+ " el.setAttribute('aria-expanded', isOpen);",
+ " if (isOpen) {",
+ " var firstItem = container.querySelector('[role=\"menuitem\"]');",
+ " if (firstItem) firstItem.focus();",
+ " }",
+ "}",
+ "",
+ "function closeStatusDropdown(container) {",
+ " container.classList.remove('open');",
+ " var badge = container.querySelector('[role=\"button\"]');",
+ " if (badge) {",
+ " badge.setAttribute('aria-expanded', 'false');",
+ " badge.focus();",
+ " }",
+ "}",
+ "",
+ "function handleStatusKeydown(event, el) {",
+ " if (event.key === 'Enter' || event.key === ' ') {",
+ " event.preventDefault();",
+ " toggleStatusDropdown(el);",
+ " } else if (event.key === 'Escape') {",
+ " closeStatusDropdown(el.parentElement);",
+ " } else if (event.key === 'ArrowDown') {",
+ " event.preventDefault();",
+ " var container = el.parentElement;",
+ " if (!container.classList.contains('open')) {",
+ " toggleStatusDropdown(el);",
+ " } else {",
+ " var firstItem = container.querySelector('[role=\"menuitem\"]');",
+ " if (firstItem) firstItem.focus();",
+ " }",
+ " }",
+ "}",
+ "",
+ "function handleMenuItemKeydown(event) {",
+ " var container = event.target.closest('.status-badge-dropdown');",
+ " var items = container.querySelectorAll('[role=\"menuitem\"]');",
+ " var currentIndex = Array.from(items).indexOf(event.target);",
+ " ",
+ " if (event.key === 'ArrowDown') {",
+ " event.preventDefault();",
+ " var next = (currentIndex + 1) % items.length;",
+ " items[next].focus();",
+ " } else if (event.key === 'ArrowUp') {",
+ " event.preventDefault();",
+ " var prev = (currentIndex - 1 + items.length) % items.length;",
+ " items[prev].focus();",
+ " } else if (event.key === 'Escape') {",
+ " event.preventDefault();",
+ " closeStatusDropdown(container);",
+ " } else if (event.key === 'Tab') {",
+ " closeStatusDropdown(container);",
+ " }",
+ "}",
+ "",
+ "document.addEventListener('click', function(e) {",
+ " var dropdowns = document.querySelectorAll('.status-badge-dropdown.open');",
+ " dropdowns.forEach(function(d) {",
+ " if (!d.contains(e.target)) {",
+ " closeStatusDropdown(d);",
+ " }",
+ " });",
+ "});"
+ ]
navbar :: (Monad m) => Lucid.HtmlT m ()
navbar =
@@ -294,20 +366,30 @@ clickableBadge status _tid =
TaskCore.Done -> ("badge badge-done status-badge-clickable", "Done")
in Lucid.span_
[ Lucid.class_ cls,
- Lucid.makeAttribute "onclick" "this.parentElement.classList.toggle('open')"
+ Lucid.tabindex_ "0",
+ Lucid.role_ "button",
+ Lucid.makeAttribute "aria-haspopup" "true",
+ Lucid.makeAttribute "aria-expanded" "false",
+ Lucid.makeAttribute "onclick" "toggleStatusDropdown(this)",
+ Lucid.makeAttribute "onkeydown" "handleStatusKeydown(event, this)"
]
<| do
Lucid.toHtml label
- Lucid.span_ [Lucid.class_ "dropdown-arrow"] " ▾"
+ Lucid.span_ [Lucid.class_ "dropdown-arrow", Lucid.makeAttribute "aria-hidden" "true"] " ▾"
statusDropdownOptions :: (Monad m) => TaskCore.Status -> Text -> Lucid.HtmlT m ()
statusDropdownOptions currentStatus tid =
- Lucid.div_ [Lucid.class_ "status-dropdown-menu"] <| do
- statusOption TaskCore.Open currentStatus tid
- statusOption TaskCore.InProgress currentStatus tid
- statusOption TaskCore.Review currentStatus tid
- statusOption TaskCore.Approved currentStatus tid
- statusOption TaskCore.Done currentStatus tid
+ Lucid.div_
+ [ Lucid.class_ "status-dropdown-menu",
+ Lucid.role_ "menu",
+ Lucid.makeAttribute "aria-label" "Change task status"
+ ]
+ <| do
+ statusOption TaskCore.Open currentStatus tid
+ statusOption TaskCore.InProgress currentStatus tid
+ statusOption TaskCore.Review currentStatus tid
+ statusOption TaskCore.Approved currentStatus tid
+ statusOption TaskCore.Done currentStatus tid
statusOption :: (Monad m) => TaskCore.Status -> TaskCore.Status -> Text -> Lucid.HtmlT m ()
statusOption opt currentStatus tid =
@@ -321,6 +403,7 @@ statusOption opt currentStatus tid =
optClass = cls <> " status-dropdown-option" <> if isSelected then " selected" else ""
in Lucid.form_
[ Lucid.class_ "status-option-form",
+ Lucid.role_ "none",
Lucid.makeAttribute "hx-post" ("/tasks/" <> tid <> "/status"),
Lucid.makeAttribute "hx-target" "#status-badge-container",
Lucid.makeAttribute "hx-swap" "outerHTML"
@@ -329,7 +412,10 @@ statusOption opt currentStatus tid =
Lucid.input_ [Lucid.type_ "hidden", Lucid.name_ "status", Lucid.value_ (tshow opt)]
Lucid.button_
[ Lucid.type_ "submit",
- Lucid.class_ optClass
+ Lucid.class_ optClass,
+ Lucid.role_ "menuitem",
+ Lucid.tabindex_ "-1",
+ Lucid.makeAttribute "onkeydown" "handleMenuItemKeydown(event)"
]
(Lucid.toHtml label)
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
index 594fb21..8ad239c 100644
--- a/Omni/Jr/Web/Style.hs
+++ b/Omni/Jr/Web/Style.hs
@@ -558,9 +558,16 @@ statusBadges = do
transition "opacity" (ms 150) ease (sec 0)
".status-dropdown-option" # hover ? do
opacity 0.7
+ ".status-dropdown-option" # focus ? do
+ opacity 0.85
+ Stylesheet.key "outline" ("2px solid #0066cc" :: Text)
+ Stylesheet.key "outline-offset" ("1px" :: Text)
".status-dropdown-option.selected" ? do
Stylesheet.key "outline" ("2px solid #0066cc" :: Text)
Stylesheet.key "outline-offset" ("1px" :: Text)
+ ".status-badge-clickable" # focus ? do
+ Stylesheet.key "outline" ("2px solid #0066cc" :: Text)
+ Stylesheet.key "outline-offset" ("2px" :: Text)
buttonStyles :: Css
buttonStyles = do