Skip to content

Commit

Permalink
Make "Rules of Engagement" toggleable
Browse files Browse the repository at this point in the history
We remember the open/closed state of the Rules of Engagement section by
storing state into cookie called "rules". A non-blank value indicates
that the section should be collapsed on page load.
- I strongly considered making this a signing-user-only feature, but it
  does make the code somewhat significantly more complex. And, in the
  end, it feels like the sort of thing that could be too annoying to
  have to look at, even for first-time visitors if they're experts at
  minesweeper already. And since there's no way to know that signing
  will give this feature, it's probably better to just go this route.

We use el-transition.js combined with TailwindCSS's `transition` class
to define and then animate the transition effects between open/closed
states.

Also, remove unused code from menu_controller.js. I noticed this when
copying code into the new collapse_controller.js. The unused code comes
from copying menu_controller.js in from another app that used it for a
bit more than just the Theme menu.
  • Loading branch information
Paul DobbinSchmaltz committed Oct 24, 2024
1 parent 0eca51d commit 6bef505
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 50 deletions.
48 changes: 48 additions & 0 deletions app/javascript/controllers/collapse_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Controller } from "@hotwired/stimulus"
import { toggle } from "el-transition"
import { cookies } from "cookies"

export default class extends Controller {
static targets = ["button", "icon", "section"]
static values = {
cookieName: String,
collapsedCookie: { type: String, default: "collapsed" },
}
static classes = ["buttonToggle", "iconToggle"]

toggle() {
this.#setCookie()
this.toggleButtonState()
toggle(this.sectionTarget)
this.#updateAriaAttributes()
}

#setCookie() {
if (this.#sectionIsHidden()) {
// -> Visible
cookies.delete(this.cookieNameValue)
} else {
// -> Hidden
cookies.permanent.set(this.cookieNameValue, this.collapsedCookieValue)
}
}

#sectionIsHidden() {
return this.sectionTarget.classList.contains("hidden")
}

toggleButtonState() {
this.buttonTarget.classList.toggle(this.buttonToggleClass)
this.iconTarget.classList.toggle(this.iconToggleClass)
}

#updateAriaAttributes() {
if (this.sectionTarget.getAttribute("aria-hidden") === "true") {
this.sectionTarget.setAttribute("aria-hidden", "false")
this.buttonTarget.setAttribute("aria-expanded", "true")
} else {
this.sectionTarget.setAttribute("aria-hidden", "true")
this.buttonTarget.setAttribute("aria-expanded", "false")
}
}
}
16 changes: 1 addition & 15 deletions app/javascript/controllers/menu_controller.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Controller } from "@hotwired/stimulus"
import { enter, leave, toggle } from "el-transition"
import { leave, toggle } from "el-transition"

export default class extends Controller {
static targets = ["menu", "button"]

toggle() {
toggle(this.menuTarget)
this.toggleButtonState()
this.#updateAriaAttributes()
}

Expand All @@ -20,22 +19,9 @@ export default class extends Controller {
}
}

toggleButtonState() {
// Implement in child classes as needed.
}

#show() {
if (this.#menuIsHidden()) {
enter(this.menuTarget)
this.toggleButtonState()
this.#updateAriaAttributes()
}
}

#hide() {
if (this.#menuIsVisible()) {
leave(this.menuTarget)
this.toggleButtonState()
this.#updateAriaAttributes()
}
}
Expand Down
109 changes: 75 additions & 34 deletions app/views/games/_rules.html.erb
Original file line number Diff line number Diff line change
@@ -1,37 +1,78 @@
<%# locals: () %>
<%# locals: (view:) %>

<div class="space-y-6">
<h3 class="h4">Rules of Engagement</h3>
<div
data-controller="collapse"
data-collapse-button-toggle-class="<%= view.collapsed_button_css_class %>"
data-collapse-icon-toggle-class="<%= view.collapsed_icon_css_class %>"
data-collapse-cookie-name-value="<%= view.cookie_name %>"
class="space-y-6"
>
<h3
data-collapse-target="button"
data-action="click->collapse#toggle"
aria-label="Toggle Rules of Engagement"
aria-haspopup="true"
aria-expanded="<%= view.open? %>"
class="
h4 flex items-center gap-x-3 cursor-pointer
transition-colors
<%= class_names(view.collapsed_button_css_class) if view.collapsed? %>
"
>
Rules of Engagement
<%= inline_svg_tag(
"heroicons/chevron-down.svg",
class:
"max-w-4 max-h-4 stroke-2 transition-transform "\
"#{class_names(view.collapsed_icon_css_class) if view.collapsed?}",
data: { collapse_target: "icon" }) %>
</h3>

<ol class="space-y-2 list-decimal ml-7">
<li><strong>Click</strong> a cell to reveal it.</li>
<li>
A revealed cell indicates the number of mines (<%= Icon.mine %>) in the cells surrounding it.
<p class="text-gray-500 dark:text-neutral-400">
&ndash; Possible values include: Blank/0 (<%= Icon.cell %>), or 1&ndash;8.
</p>
</li>
<li><strong>Right Click</strong> a cell to flag (<%= Icon.flag %>) it as a suspected mine (<%= Icon.mine %>).</li>
<li>
<strong>Click</strong> a revealed cell to reveal all possible surrounding cells (this is called "chording").
<p class="text-gray-500 dark:text-neutral-400">
&ndash; Possible only after the correct number of surrounding cells have been flagged.
</p>
<p class="text-gray-500 dark:text-neutral-400">
&ndash; Be careful! This does not protect against mistaken flag (<%= Icon.flag %>) placements.
</p>
</li>
<li>The game is <strong>lost</strong> if a mine (<%= Icon.mine %>) is revealed.</li>
<li>
The game is <strong>won</strong> when all non-mine cells are revealed.
<p class="text-gray-500 dark:text-neutral-400">
&ndash; Regardless of how many flags have been placed.
</p>
</li>
</ol>
</div>
<div
data-collapse-target="section"
data-transition-enter="transition ease-out duration-100"
data-transition-enter-start="transform opacity-0 scale-95"
data-transition-enter-end="transform opacity-100 scale-100"
data-transition-leave="transition ease-in duration-75"
data-transition-leave-start="transform opacity-100 scale-100"
data-transition-leave-end="transform opacity-0 scale-95"
aria-hidden="<%= view.collapsed? %>"
aria-orientation="vertical"
class="
space-y-10
<%= class_names(view.collapsed_section_css_class) if view.collapsed? %>
"
>
<ol class="space-y-2 list-decimal ml-7">
<li><strong>Click</strong> a cell to reveal it.</li>
<li>
A revealed cell indicates the number of mines (<%= Icon.mine %>) in the cells surrounding it.
<p class="text-gray-500 dark:text-neutral-400">
&ndash; Possible values include: Blank/0 (<%= Icon.cell %>), or 1&ndash;8.
</p>
</li>
<li><strong>Right Click</strong> a cell to flag (<%= Icon.flag %>) it as a suspected mine (<%= Icon.mine %>).</li>
<li>
<strong>Click</strong> a revealed cell to reveal all possible surrounding cells (this is called "chording").
<p class="text-gray-500 dark:text-neutral-400">
&ndash; Possible only after the correct number of surrounding cells have been flagged.
</p>
<p class="text-gray-500 dark:text-neutral-400">
&ndash; Be careful! This does not protect against mistaken flag (<%= Icon.flag %>) placements.
</p>
</li>
<li>The game is <strong>lost</strong> if a mine (<%= Icon.mine %>) is revealed.</li>
<li>
The game is <strong>won</strong> when all non-mine cells are revealed.
<p class="text-gray-500 dark:text-neutral-400">
&ndash; Regardless of how many flags have been placed.
</p>
</li>
</ol>

<p class="mt-10">
<%= Icon.clover %> Good luck!
The Alliance is counting on you<span class="font-sans">&hellip;</span>
</p>
<p>
<%= Icon.clover %> Good luck!
The Alliance is counting on you<span class="font-sans">&hellip;</span>
</p>
</div>
</div>
28 changes: 28 additions & 0 deletions app/views/games/rules.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

# :reek:RepeatedConditional

# Games::Rules is a View Model for managing display of the game-play rules
# section.
class Games::Rules
COOKIE_NAME = "rules"

def initialize(context:)
@context = context
end

def cookie_name = COOKIE_NAME

def collapsed_button_css_class = "text-gray-500"
def collapsed_icon_css_class = "-rotate-90"
def collapsed_section_css_class = "hidden"

def open? = !collapsed?
def collapsed? = cookies[cookie_name].present?

private

attr_reader :context

def cookies = context.cookies
end
2 changes: 1 addition & 1 deletion app/views/home/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<% if @view.game_just_ended? %>
<%= render("games/results", view: @view.results(user: current_user)) %>
<% else %>
<%= render("games/rules") %>
<%= render("games/rules", view: @view.rules(context: layout)) %>
<% end %>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions app/views/home/show.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def new_game_view
Games::New.new
end

def rules(context:)
Games::Rules.new(context:)
end

def current_game? = !!current_game

def game_just_ended?
Expand Down

0 comments on commit 6bef505

Please sign in to comment.