Merge branch 'main' into card-steps

* main: (22 commits)
  Fix that stage should be visible above card background
  Indicate opens menu
  Hide dialog targets from screen readers when closed
  Mark up as heading for screen reader navigation
  Copy for screen readers
  Don't hide the input so it's accessible to screen readers
  Point skip links to correct anchor, don't navigate with turbo
  There aren't any `li` children so just use `div`
  Use headings for screen reader navigation
  Use `h3` for individual cards in collection view
  Make collection title `h1` for screen reader navigation
  Esc should not propagate when closing dialogs
  squash! Remove session controller actions other than delete
  Screen reader: Use heading for comments navigation
  Remove the invitation codes for now
  Remove session controller actions other than delete
  Correct label for field
  Style
  Screen reader: De-duplicate links for each person in account settings
  Account.signal_account is no longer optional
  ...
This commit is contained in:
Jason Zimdars
2025-07-02 16:09:49 -05:00
40 changed files with 88 additions and 168 deletions
+1 -1
View File
@@ -85,7 +85,7 @@
.card__background {
.card:has(& img:not([src=""])) {
.card__body,
.card__content,
.card__meta,
.workflow-stage:not(.workflow-stage--current) {
opacity: 0;
+8 -2
View File
@@ -37,8 +37,14 @@
}
.comment__author {
.comment--system & strong {
display: none;
.comment--system & {
h3 {
margin-inline: auto;
}
strong {
display: none;
}
}
.btn {
+4 -4
View File
@@ -37,7 +37,6 @@
}
.input--file {
border: 0;
cursor: pointer;
display: grid;
inline-size: auto;
@@ -52,6 +51,7 @@
}
input[type="file"] {
--hover-size: 0;
--input-border-color: transparent;
--input-border-radius: 8px;
@@ -69,9 +69,9 @@
&:focus,
&:focus-visible {
--input-border-color: var(--color-selected-dark);
--input-border-radius: 8px;
--input-border-size: 0.15rem;
.input--file:has(&) {
outline: 0.15rem solid var(--color-selected-dark);
}
}
}
+1 -5
View File
@@ -48,11 +48,7 @@ module Authentication
def request_authentication
session[:return_to_after_authenticating] = request.url
if Rails.application.config.x.local_authentication
redirect_to new_session_path
else
redirect_to Launchpad.login_url(account: Current.account), allow_other_host: true
end
redirect_to Launchpad.login_url(account: Current.account), allow_other_host: true
end
def after_authentication_url
@@ -1,8 +1,7 @@
class Sessions::LaunchpadController < ApplicationController
require_unauthenticated_access
before_action :store_sig, only: :show
before_action :restore_and_clear_sig, only: :update
before_action :require_sig
def show
end
@@ -17,11 +16,7 @@ class Sessions::LaunchpadController < ApplicationController
end
private
def store_sig
cookies[:_fizzy_launchpad_sig] = params.expect(:sig)
end
def restore_and_clear_sig
@sig = cookies.delete :_fizzy_launchpad_sig
def require_sig
@sig = params.expect(:sig)
end
end
+1 -23
View File
@@ -1,28 +1,6 @@
class SessionsController < ApplicationController
require_unauthenticated_access only: %i[ new create ]
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
before_action :require_first_run_completion
def new
end
def create
if user = User.active.authenticate_by(params.permit(:email_address, :password))
start_new_session_for user
redirect_to after_authentication_url
else
redirect_to new_session_path, alert: "Try another email address or password."
end
end
def destroy
terminate_session
redirect_to new_session_path
redirect_to Launchpad.login_url, allow_other_host: true
end
private
def require_first_run_completion
redirect_to first_run_path if Account.none?
end
end
@@ -1,6 +1,5 @@
class Signup::BaseController < ApplicationController
require_untenanted_access
before_action :redirect_if_local_auth
# TODO: Remove this auth before launch.
http_basic_authenticate_with(
@@ -34,8 +33,4 @@ class Signup::BaseController < ApplicationController
redirect_to account.signal_account.owner.remote_login_url(proceed_to: root_path),
allow_other_host: true
end
def redirect_if_local_auth
render plain: "Unauthorized", status: :unauthorized if Rails.application.config.x.local_authentication
end
end
+1 -1
View File
@@ -37,7 +37,7 @@ module EventsHelper
def event_cluster_tag(hour, col, &)
row = 25 - hour
tag.ul class: "events__time-block", style: "grid-area: #{row}/#{col}", &
tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", &
end
def event_next_page_link(next_day)
@@ -6,6 +6,10 @@ export default class extends Controller {
modal: { type: Boolean, default: false }
}
connect() {
this.dialogTarget.setAttribute('aria-hidden', 'true')
}
open() {
const modal = this.modalValue
@@ -14,6 +18,7 @@ export default class extends Controller {
} else {
this.dialogTarget.show()
}
this.dialogTarget.setAttribute('aria-hidden', 'false')
}
toggle() {
@@ -26,6 +31,7 @@ export default class extends Controller {
close() {
this.dialogTarget.close()
this.dialogTarget.setAttribute('aria-hidden', 'true')
this.dialogTarget.blur()
}
+1 -4
View File
@@ -2,10 +2,7 @@ module Account::SignalAccount
extend ActiveSupport::Concern
included do
unless Rails.application.config.x.local_authentication
# TODO: remove the "optional: true" once we've populated the accounts properly
belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id, optional: true
end
belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id
end
class_methods do
+1 -3
View File
@@ -2,8 +2,6 @@ module User::SignalUser
extend ActiveSupport::Concern
included do
unless Rails.application.config.x.local_authentication
belongs_to :signal_user, dependent: :destroy, class_name: "SignalId::User", optional: true
end
belongs_to :signal_user, dependent: :destroy, class_name: "SignalId::User", optional: true
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
<li class="flex align-center gap-half pad-inline" data-filter-target="item">
<%= avatar_tag user %>
<%= avatar_tag user, hidden_for_screen_reader: true %>
<strong class="overflow-ellipsis">
<%= link_to user.name, user, class: "txt-ink btn btn--plain" %>
+1 -1
View File
@@ -1,6 +1,6 @@
<div class="panel shadow center flex flex-column">
<strong class="divider txt-large margin-block-end">People on the account</strong>
<%= render "accounts/invite" %>
<%#= render "accounts/invite" # Scheduled work in https://3.basecamp.com/2914079/buckets/37331921/todos/8746162337 %>
<div class="flex flex-column" data-controller="filter">
<div class="pad">
+9 -7
View File
@@ -8,14 +8,16 @@
<div class="comment__content flex flex-column flex-item-grow full-width">
<div class="comment__author flex align-center gap-half">
<strong>
<%= link_to comment.creator.name, comment.creator, class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %>
</strong>
<h3 class="font-weight-normal txt-normal flex-item-justify-start">
<strong>
<%= link_to comment.creator.name, comment.creator, class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %>
</strong>
<%= link_to comment, class: "txt-undecorated txt-ink translucent txt-normal txt-capitalize flex-item-justify-start" do %>
<%= local_datetime_tag comment.created_at, style: :agoorweekday %>,
<%= local_datetime_tag comment.created_at, style: :time %>
<% end %>
<%= link_to comment, class: "txt-undecorated txt-ink translucent txt-normal txt-capitalize" do %>
<%= local_datetime_tag comment.created_at, style: :agoorweekday %>,
<%= local_datetime_tag comment.created_at, style: :time %>
<% end %>
</h3>
<%= link_to edit_card_comment_path(comment.card, comment),
class: "comment__edit btn btn--circle borderless fill-transparent translucent" do %>
@@ -1,4 +1,4 @@
<div class="quick-reaction" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<div class="quick-reaction" data-controller="dialog" data-action="keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside">
<%= yield %>
<dialog class="quick-reaction__popup popup panel fill-white shadow" data-dialog-target="dialog">
+1 -1
View File
@@ -7,7 +7,7 @@
<% end %>
</div>
<% else %>
<details class="expander" data-controller="details" data-details-target="details" data-action="keydown.esc->details#close click@document->details#closeOnClickOutside">
<details class="expander" data-controller="details" data-details-target="details" data-action="keydown.esc->details#close:stop click@document->details#closeOnClickOutside">
<summary class="expander__button btn borderless">
Close as
<%= icon_tag "caret-down" %>
+2 -3
View File
@@ -5,13 +5,12 @@
<% end %>
<% elsif !card.closed? %>
<%= form_with model: card, url: collection_card_path(card.collection, card), data: { controller: "form" } do |form| %>
<label class="btn">
<label class="btn input--file">
<%= icon_tag "picture-add" %>
<span class="for-screen-reader">Add background image</span>
<%= form.file_field :image, class: "input",
accept: "image/png, image/jpeg, image/jpg, image/webp",
data: { action: "upload-preview#previewImage form#submit", upload_preview_target: "input" },
hidden: true %>
data: { action: "upload-preview#previewImage form#submit", upload_preview_target: "input" } %>
</label>
<% end %>
<% end %>
+2 -2
View File
@@ -17,9 +17,9 @@
<div class="card__body justify-space-between">
<div class="card__content">
<h1 class="card__title overflow-line-clamp">
<h3 class="card__title overflow-line-clamp">
<%= card.title %>
</h1>
</h3>
<%= link_to collection_card_path(card.collection, card), draggable: false, class: "card__link", title: card_title_tag(card), data: { action: "dialog#close", turbo_frame: "_top" } do %>
<span class="for-screen-reader"><%= card.title %></span>
+2 -2
View File
@@ -7,9 +7,9 @@
<% end %>
<header class="margin-block-end">
<span class="txt-large overflow-ellipsis margin-none font-black" style="view-transistion-name: card-collection-title">
<h1 class="txt-large overflow-ellipsis margin-none font-black" style="view-transistion-name: card-collection-title">
<span><%= filter_title @filter %></span>
</span>
</h1>
<%= render "filters/settings", filter: @filter %>
</header>
+2 -2
View File
@@ -6,7 +6,7 @@
terminal_target: "form",
action: "turbo:submit-start->terminal#commandSubmitted turbo:submit-end->terminal#focus keydown.meta+k@document->terminal#focus:prevent keydown.ctrl+k@document->terminal#focus:prevent keydown.enter->terminal#executeCommand keydown.esc->terminal#hideMenus turbo:submit-end->terminal#handleCommandResponse"
} do %>
<label class="terminal__label txt-nowrap" for="fizzy_do">Fizzy do &gt;</label>
<label class="terminal__label txt-nowrap" for="command">Fizzy do &gt;</label>
<%= text_field_tag "command", nil,
autocomplete: "off",
@@ -20,5 +20,5 @@
placeholder: platform.desktop? ? "Press #{ hotkey_label(["ctrl", "K"]) } to search or type commands…" : "Search or type commands…",
spellcheck: "false" %>
<%= hidden_field_tag "confirmed", nil, data: { terminal_target: "confirmation" } %>
<%= hidden_field_tag "confirmed", nil, data: { terminal_target: "confirmation" } %>
<% end %>
+2 -2
View File
@@ -6,12 +6,12 @@
related_element_group_value: card.id,
action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
<div class="card__header flex align-start gap full-width">
<div class="card__collection flex-inline align-start">
<h4 class="card__collection flex-inline align-start">
<span class="card__id"><%= card.id %></span>
<span class="card__collection-name">
<span class="overflow-ellipsis"><%= event.collection.name %></span>
</span>
</div>
</h4>
<% if event_column(event) == 2 %>
<span class="event__icon flex-item-justify-end txt-reversed">
+1 -1
View File
@@ -4,7 +4,7 @@
<body data-controller="lightbox local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
<header id="header">
<a href="#main-content" class="skip-navigation btn">Skip to main content</a>
<a href="#main" class="skip-navigation btn" data-turbo="false">Skip to main content</a>
<%= yield :header %>
</header>
+1 -1
View File
@@ -4,7 +4,7 @@
<body class="public" data-controller="local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
<header id="header">
<a href="#main-content" class="skip-navigation btn">Skip to main content</a>
<a href="#main" class="skip-navigation btn" data-turbo="false">Skip to main content</a>
<%= yield :header %>
</header>
+1 -1
View File
@@ -12,7 +12,7 @@
</button>
</div>
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand pins stack">
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand pins stack" aria-haspopup="true">
<%= icon_tag "pinned" %>
</button>
</section>
@@ -10,9 +10,9 @@
<%= avatar_image_tag notification.creator %>
</div>
<div class="flex flex-column min-width flex-item-grow">
<h3 class="flex flex-column min-width flex-item-grow font-weight-normal">
<%= render "notifications/notification/#{notification.source_type.underscore}/body", notification: notification %>
</div>
</h3>
</div>
<% end %>
<% end %>
+2 -2
View File
@@ -16,7 +16,7 @@
<div class="tray__actions">
<%= button_to read_all_notifications_path,
class: "tray__notification-read-action btn btn--reversed fill-transparent borderless flex-item-justify-start txt-xx-small",
title: "Mark all as read",
title: "Mark all notifications as read",
data: { action: "click->dialog#close", turbo_frame: "notifications" } do %>
<%= icon_tag "check" %>
<% end %>
@@ -34,7 +34,7 @@
<% end %>
</div>
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand notifications stack">
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand notifications stack" aria-haspopup="true">
<%= icon_tag "bell" %>
</button>
</section>
+3 -1
View File
@@ -1 +1,3 @@
<%= auto_submit_form_with method: :put %>
<%= auto_submit_form_with(method: :put) do |form| %>
<%= form.hidden_field(:sig, value: @sig) %>
<% end %>
-30
View File
@@ -1,30 +0,0 @@
<% @page_title = "Sign in" %>
<div class="panel shadow center margin-block-double <%= "shake" if flash[:alert] %>" style="--panel-size: 55ch;">
<h1 class="txt-xx-large margin-block-end-double">Fizzy</h1>
<%= form_with url: session_path, class: "flex flex-column gap txt-large" do |form| %>
<div class="flex align-center gap">
<label class="flex align-center gap input input--actor">
<%= form.email_field :email_address, required: true, class: "input full-width", autofocus: true, autocomplete: "username", placeholder: "Enter your email address" %>
<%= icon_tag "email", class: "txt-large" %>
</label>
</div>
<div class="flex align-center gap">
<label class="flex align-center gap input input--actor">
<%= form.password_field :password, required: true, class: "input full-width", autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %>
<%= icon_tag "password", class: "txt-large" %>
</label>
</div>
<button type="submit" id="log_in" class="btn btn--reversed center">
<%= icon_tag "arrow-right" %>
<span class="for-screen-reader">Sign in</span>
</button>
<% end %>
</div>
<% content_for :footer do %>
<div class="txt-align-center center margin-block-double txt-subtle">Fizzy&trade; version 0</div>
<% end %>
+1 -1
View File
@@ -2,7 +2,7 @@
<% content_for :header do %>
<nav>
<%= link_to new_session_path, class: "btn flex-item-justify-end", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= link_to Launchpad.login_url(account: Current.account), class: "btn flex-item-justify-end", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="for-screen-reader">Sign in instead</span>
<% end %>
</nav>
+1 -1
View File
@@ -43,7 +43,7 @@
</div>
<div class="panel borderless center margin-block-double">
<%= button_to session_path, method: :delete, class: "btn center txt-medium" do %>
<%= button_to session_path, method: :delete, class: "btn center txt-medium", data: { turbo: false } do %>
<%= icon_tag "logout" %>
<span>Sign out</span>
<% end %>
-7
View File
@@ -1,18 +1,11 @@
require "signal_id"
Rails.application.config.x.local_authentication = ENV["LOCAL_AUTHENTICATION"].present?
ENV["SIGNAL_ID_SECRET"] = Rails.application.credentials.signal_id_secret
Rails.application.config.to_prepare do
SignalId.product = "fizzy"
db_config = SignalId::Database.default_configuration
if Rails.application.config.x.local_authentication
db_config.each do |name, config|
config["connect_timeout"] = 1
end
end
SignalId::Database.load_configuration db_config
SignalId::Database.enable_rw_splitting!
+2 -2
View File
@@ -85,11 +85,11 @@ Rails.application.routes.draw do
get "join/:join_code", to: "users#new", as: :join
post "join/:join_code", to: "users#create"
resource :session do
resource :session, only: :destroy do
scope module: "sessions" do
resources :transfers, only: %i[ show update ]
resource :launchpad, only: %i[ show update ], controller: "launchpad"
end
resource :launchpad, only: %i[ show update ], controller: "sessions/launchpad"
end
namespace :signup do
@@ -0,0 +1,5 @@
class RemovePasswordFromUsers < ActiveRecord::Migration[8.1]
def change
remove_column :users, :password, :string
end
end
@@ -0,0 +1,5 @@
class AddNotNullToAccountsQueenbeeId < ActiveRecord::Migration[8.1]
def change
change_column_null :accounts, :queenbee_id, false
end
end
Generated
+2 -2
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2025_06_25_120000) do
ActiveRecord::Schema[8.1].define(version: 2025_07_02_160230) do
create_table "accesses", force: :cascade do |t|
t.datetime "accessed_at"
t.integer "collection_id", null: false
@@ -28,7 +28,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_25_120000) do
t.datetime "created_at", null: false
t.string "join_code"
t.string "name", null: false
t.integer "queenbee_id"
t.integer "queenbee_id", null: false
t.datetime "updated_at", null: false
t.index ["queenbee_id"], name: "index_accounts_on_queenbee_id", unique: true
end
+2 -2
View File
@@ -128,7 +128,7 @@ columns:
name: queenbee_id
cast_type: *3
sql_type_metadata: *4
'null': true
'null': false
default:
default_function:
collation:
@@ -2451,4 +2451,4 @@ indexes:
comment:
valid: true
workflows: []
version: 20250625120000
version: 20250702160230
+2 -2
View File
@@ -11,7 +11,7 @@ class EventsControllerTest < ActionDispatch::IntegrationTest
test "index" do
get events_path
assert_select "ul.events__time-block[style='grid-area: 17/2']" do
assert_select "div.events__time-block[style='grid-area: 17/2']" do
assert_select "strong", text: "David assigned JZ to Layout is broken"
end
end
@@ -21,7 +21,7 @@ class EventsControllerTest < ActionDispatch::IntegrationTest
get events_path
assert_select "ul.events__time-block[style='grid-area: 22/2']" do
assert_select "div.events__time-block[style='grid-area: 22/2']" do
assert_select "strong", text: "David assigned JZ to Layout is broken"
end
end
@@ -6,14 +6,16 @@ class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal cookies[:_fizzy_launchpad_sig], "test-sig"
assert_select "form input#sig" do |node|
assert_equal node.length, 1
assert_equal node.first["value"], "test-sig"
end
end
test "create establishes a session when the sig is valid" do
user = users(:david)
cookies[:_fizzy_launchpad_sig] = user.signal_user.perishable_signature
put session_launchpad_path
put session_launchpad_path(params: { sig: user.signal_user.perishable_signature })
assert_redirected_to root_url
assert parsed_cookies.signed[:session_token]
@@ -21,9 +23,8 @@ class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest
test "returns 401 when the sig is invalid" do
user = users(:david)
cookies[:sig] = "not-valid"
put session_launchpad_path
put session_launchpad_path(params: { sig: "invalid" })
assert_response :unauthorized
end
+1 -29
View File
@@ -1,38 +1,10 @@
require "test_helper"
class SessionsControllerTest < ActionDispatch::IntegrationTest
ALLOWED_BROWSER = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15"
DISALLOWED_BROWSER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0"
test "new" do
get new_session_path
assert_response :success
end
test "new enforces browser compatibility" do
get new_session_path, env: { "HTTP_USER_AGENT" => DISALLOWED_BROWSER }
assert_select "svg", message: /Your browser is not supported/
get new_session_path, env: { "HTTP_USER_AGENT" => ALLOWED_BROWSER }
assert_select "svg", text: /Your browser is not supported/, count: 0
end
test "create with valid credentials" do
post session_path, params: { email_address: "david@37signals.com", password: "secret123456" }
assert_redirected_to root_path
assert cookies[:session_token].present?
end
test "create with invalid credentials" do
post session_path, params: { email_address: "david@37signals.com", password: "wrong" }
assert_redirected_to new_session_path
assert_not cookies[:session_token].present?
end
test "destroy" do
sign_in_as :kevin
delete session_path
assert_redirected_to new_session_path
assert_redirected_to Launchpad.login_url
assert_not cookies[:session_token].present?
end
end
+1 -1
View File
@@ -6,7 +6,7 @@ module SessionTestHelper
def sign_in_as(user)
cookies[:session_token] = nil
user = users(user) unless user.is_a? User
post session_path, params: { email_address: user.email_address, password: "secret123456" }
put session_launchpad_path, params: { sig: user.signal_user.perishable_signature }
assert cookies[:session_token].present?
end