Skip to content

Commit ebab85a

Browse files
authoredAug 19, 2024··
[SelectPanel] Fix issue causing only one server-rendered item to appear selected (#3010)
Co-authored-by: camertron <camertron@users.noreply.github.com>
1 parent 2cba6d9 commit ebab85a

32 files changed

+114
-38
lines changed
 

‎.changeset/brown-tables-do.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@primer/view-components': patch
3+
---
4+
5+
Fixes several SelectPanel bugs:
6+
7+
1. If multiple server-rendered items are checked, the panel will only show one item checked.
8+
2. If no `data-value` attributes are provided, panels in single-select mode will allow multiple items to be checked.

‎app/components/primer/alpha/select_panel_element.ts

+15-14
Original file line numberDiff line numberDiff line change
@@ -713,11 +713,12 @@ export class SelectPanelElement extends HTMLElement {
713713
itemContent.setAttribute(this.ariaSelectionType, 'false')
714714
}
715715
} else if (value && !this.#selectedItems.has(value) && this.isItemChecked(item)) {
716-
this.#hasLoadedData = true
717716
this.#addSelectedItem(item)
718717
}
719718
}
720719

720+
this.#hasLoadedData = true
721+
721722
if (!this.noResults) return
722723

723724
if (this.#inErrorState()) {
@@ -860,7 +861,8 @@ export class SelectPanelElement extends HTMLElement {
860861
// interfere with events fired by menu items whose behavior is specified outside the library.
861862
if (this.selectVariant !== 'multiple' && this.selectVariant !== 'single') return
862863

863-
const checked = !this.isItemChecked(item)
864+
const currentlyChecked = this.isItemChecked(item)
865+
const checked = !currentlyChecked
864866

865867
const activationSuccess = this.dispatchEvent(
866868
new CustomEvent('beforeItemActivated', {
@@ -875,22 +877,21 @@ export class SelectPanelElement extends HTMLElement {
875877
const itemContent = this.#getItemContent(item)
876878

877879
if (this.selectVariant === 'single') {
878-
const value = this.selectedItems[0]?.value
879-
const element = this.visibleItems.find(el => this.#getItemContent(el)?.getAttribute('data-value') === value)
880+
// disallow unchecking checked item in single-select mode
881+
if (!currentlyChecked) {
882+
for (const el of this.items) {
883+
this.#getItemContent(el)?.setAttribute(this.ariaSelectionType, 'false')
884+
}
880885

881-
if (element) {
882-
this.#getItemContent(element)?.setAttribute(this.ariaSelectionType, 'false')
883-
}
886+
this.#selectedItems.clear()
884887

885-
this.#selectedItems.clear()
888+
if (checked) {
889+
this.#addSelectedItem(item)
890+
itemContent?.setAttribute(this.ariaSelectionType, 'true')
891+
}
886892

887-
// Only check, never uncheck here. Single-select mode does not allow unchecking a checked item.
888-
if (checked) {
889-
this.#addSelectedItem(item)
890-
itemContent?.setAttribute(this.ariaSelectionType, 'true')
893+
this.#setDynamicLabel()
891894
}
892-
893-
this.#setDynamicLabel()
894895
} else {
895896
// multi-select mode allows unchecking a checked item
896897
itemContent?.setAttribute(this.ariaSelectionType, `${checked}`)

‎demo/app/controllers/select_panel_items_controller.rb

+24-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class SelectPanelItemsController < ApplicationController
55
SELECT_PANEL_ITEMS = [
66
{ value: 1, title: "Photon torpedo", description: "Starship-mounted missile" },
77
{ value: 2, title: "Bat'leth", description: "The Klingon warrior's preferred means of achieving honor" },
8-
{ value: 3, selected: true, title: "Phaser", description: "The iconic handheld laser beam" },
8+
{ value: 3, title: "Phaser", description: "The iconic handheld laser beam" },
99
{ value: 4, title: "Lightsaber", description: "An elegant weapon for a more civilized age", recent: true },
1010
{ value: 5, title: "Proton pack", description: "Ghostbusting equipment" },
1111
{ value: 6, title: "Sonic screwdriver", description: "The Time Lord's multi-purpose tool" },
@@ -42,9 +42,13 @@ def index
4242
[]
4343
end
4444

45-
if unselect_items?
46-
results.map! do |result|
47-
result.merge(selected: false)
45+
if allows_selection?
46+
results = results.map(&:dup)
47+
results.each do |result|
48+
if selected_items.any? { |item| result[:title].downcase.include?(item) }
49+
result[:selected] = true
50+
break if single_select?
51+
end
4852
end
4953
end
5054

@@ -91,11 +95,23 @@ def key_for(uuid)
9195
"#{COOKIE_PREFIX}#{uuid}"
9296
end
9397

94-
def select_items?
95-
params.fetch(:select_items, "true") == "true"
98+
def selected_items
99+
params.fetch(:selected_items, "").downcase.split(",").map(&:strip)
96100
end
97101

98-
def unselect_items?
99-
!select_items?
102+
def select_variant
103+
@select_variant ||= params.fetch(:select_variant, Primer::Alpha::SelectPanel::DEFAULT_SELECT_VARIANT).to_sym
104+
end
105+
106+
def single_select?
107+
select_variant == :single
108+
end
109+
110+
def multi_select?
111+
select_variant == :multiple
112+
end
113+
114+
def allows_selection?
115+
Primer::Alpha::SelectPanel::SELECT_VARIANT_OPTIONS.include?(select_variant)
100116
end
101117
end

‎previews/primer/alpha/select_panel_preview.rb

+14-9
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class SelectPanelPreview < ViewComponent::Preview
1818
# @param open_on_load toggle
1919
# @param anchor_align [Symbol] select [start, center, end]
2020
# @param anchor_side [Symbol] select [outside_bottom, outside_top, outside_left, outside_right]
21-
# @param select_items toggle
21+
# @param selected_items text
2222
def playground(
2323
title: "Sci-fi equipment",
2424
subtitle: "Various tools from your favorite shows",
@@ -33,11 +33,11 @@ def playground(
3333
open_on_load: false,
3434
anchor_align: :start,
3535
anchor_side: :outside_bottom,
36-
select_items: true
36+
selected_items: "Phaser"
3737
)
3838
render_with_template(locals: {
3939
subtitle: subtitle,
40-
select_items: select_items,
40+
selected_items: selected_items,
4141
system_arguments: {
4242
title: title,
4343
size: size,
@@ -65,8 +65,6 @@ def default(open_on_load: false)
6565
})
6666
end
6767

68-
# @!group Fetch strategies
69-
7068
# @label Local fetch
7169
#
7270
# @snapshot interactive
@@ -87,8 +85,9 @@ def eventually_local_fetch(open_on_load: false)
8785
#
8886
# @snapshot interactive
8987
# @param open_on_load toggle
90-
def remote_fetch(open_on_load: false)
91-
render_with_template(locals: { open_on_load: open_on_load })
88+
# @param selected_items text
89+
def remote_fetch(open_on_load: false, selected_items: "Phaser")
90+
render_with_template(locals: { open_on_load: open_on_load, selected_items: selected_items })
9291
end
9392

9493
# @label Local fetch (no results)
@@ -115,8 +114,6 @@ def remote_fetch_no_results(open_on_load: false)
115114
render_with_template(locals: { open_on_load: open_on_load })
116115
end
117116

118-
# @!endgroup
119-
120117
# @label Single select
121118
#
122119
# @snapshot interactive
@@ -245,6 +242,14 @@ def multiselect_form(open_on_load: false, route_format: :html)
245242
def list_of_links(open_on_load: false)
246243
render_with_template(locals: { open_on_load: open_on_load })
247244
end
245+
246+
# @label No values
247+
#
248+
# @snapshot interactive
249+
# @param open_on_load toggle
250+
def no_values(open_on_load: false)
251+
render_with_template(locals: { open_on_load: open_on_load })
252+
end
248253
end
249254
end
250255
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<% subject_id = SecureRandom.hex %>
2+
3+
<%= render(Primer::Alpha::SelectPanel.new(
4+
data: { interaction_subject: subject_id },
5+
select_variant: :single,
6+
fetch_strategy: :local,
7+
open_on_load: open_on_load
8+
)) do |panel| %>
9+
<% panel.with_show_button { "Choose item" } %>
10+
<% panel.with_item(label: "Item 1") %>
11+
<% panel.with_item(label: "Item 2") %>
12+
<% panel.with_item(label: "Item 3") %>
13+
<% panel.with_item(label: "Item 4") %>
14+
<% panel.with_footer(show_divider: true) do %>
15+
I'm a footer!
16+
<% end %>
17+
<% end %>
18+
19+
<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %>

‎previews/primer/alpha/select_panel_preview/playground.html.erb

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
select_variant: :single,
1010
show_results: !simulate_no_results,
1111
fail: simulate_failure,
12-
select_items: select_items
12+
selected_items: selected_items
1313
),
1414
select_variant: :single,
1515
fetch_strategy: :remote,

‎previews/primer/alpha/select_panel_preview/remote_fetch.html.erb

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
<%= render(Primer::Alpha::SelectPanel.new(
44
data: { interaction_subject: subject_id },
5-
src: select_panel_items_path,
5+
src: select_panel_items_path(
6+
select_variant: :multiple,
7+
selected_items: selected_items,
8+
),
69
select_variant: :multiple,
710
fetch_strategy: :remote,
811
open_on_load: open_on_load

‎previews/primer/alpha/select_panel_preview/single_select.html.erb

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
open_on_load: open_on_load
88
)) do |panel| %>
99
<% panel.with_show_button { "Choose item" } %>
10-
<% panel.with_item(label: "Item 1", item_id: :item1) %>
11-
<% panel.with_item(label: "Item 2", item_id: :item2) %>
12-
<% panel.with_item(label: "Item 3", item_id: :item3) %>
13-
<% panel.with_item(label: "Item 4", item_id: :item4) %>
10+
<% panel.with_item(label: "Item 1", item_id: :item1, content_arguments: { data: { value: 1 } }) %>
11+
<% panel.with_item(label: "Item 2", item_id: :item2, content_arguments: { data: { value: 2 } }) %>
12+
<% panel.with_item(label: "Item 3", item_id: :item3, content_arguments: { data: { value: 3 } }) %>
13+
<% panel.with_item(label: "Item 4", item_id: :item4, content_arguments: { data: { value: 4 } }) %>
1414
<% panel.with_item(label: "Disabled", disabled: true, item_id: :disabled) %>
1515
<% panel.with_footer(show_divider: true) do %>
1616
I'm a footer!

‎test/system/alpha/select_panel_test.rb

+25-1
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,18 @@ def test_pressing_enter_in_filter_input_navigates_if_first_item_is_link
266266
assert_current_path "https://github.com"
267267
end
268268

269+
def test_selecting_without_data_values
270+
visit_preview(:no_values)
271+
272+
click_on_invoker_button
273+
click_on_first_item
274+
assert_selector "[aria-selected=true]", text: "Item 1", count: 1, visible: :hidden
275+
276+
click_on_invoker_button
277+
click_on_second_item
278+
assert_selector "[aria-selected=true]", text: "Item 2", count: 1, visible: :hidden
279+
end
280+
269281
########## SINGLE SELECT TESTS ############
270282

271283
def test_single_select_item_checked
@@ -471,7 +483,7 @@ def test_single_select_remembers_only_one_checked_item_and_ignores_checked_items
471483

472484
def test_single_select_handles_all_options_unselected_by_default
473485
# playground is single-select
474-
visit_preview(:playground, select_items: false)
486+
visit_preview(:playground, selected_items: "")
475487

476488
wait_for_items_to_load do
477489
click_on_invoker_button
@@ -632,6 +644,18 @@ def test_multi_select_does_not_allow_server_to_check_items_on_filter_if_selectio
632644
assert_selector "[aria-checked=true]", text: "Photon torpedo"
633645
end
634646

647+
def test_multi_select_allows_server_to_check_multiple_items
648+
# "ph" should match two items, i.e. the server should respond with two checked items
649+
visit_preview(:remote_fetch, selected_items: "ph")
650+
651+
wait_for_items_to_load do
652+
click_on_invoker_button
653+
end
654+
655+
assert_selector "[aria-checked=true]", text: "Phaser"
656+
assert_selector "[aria-checked=true]", text: "Photon torpedo"
657+
end
658+
635659
########## JAVASCRIPT API TESTS ############
636660

637661
def test_disable_item_via_js_api

0 commit comments

Comments
 (0)
Please sign in to comment.