Skip to content

Commit d2d86d2

Browse files
committed
Introduce possibility to let the server highlight search results for async FilterableTreeView
1 parent 3243927 commit d2d86d2

11 files changed

Lines changed: 112 additions & 9 deletions

File tree

.changeset/cute-yaks-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openproject/primer-view-components': minor
3+
---
4+
5+
Introduce possibility to let the server highlight search results for async FilterableTreeView

app/components/primer/open_project/filterable_tree_view.pcss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ filterable-tree-view {
3030

3131
/* Highlight style for CSS Custom Highlight API */
3232
::highlight(primer-filterable-tree-view-search-results) {
33-
background-color: var(--bgColor-attention-muted);
33+
background-color: var(--display-yellow-scale-2);
3434
}
3535

3636
/* Fallback: <mark> elements used when CSS Custom Highlight API is unavailable */
3737
/* stylelint-disable-next-line selector-max-type */
3838
filterable-tree-view mark {
39-
background-color: var(--bgColor-attention-muted);
39+
background-color: var(--display-yellow-scale-2);
4040
color: inherit;
4141
}
4242

app/components/primer/open_project/filterable_tree_view.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ class FilterableTreeView < Primer::Component
189189
DEFAULT_NO_RESULTS_NODE_ARGUMENTS.freeze
190190

191191
# @param src [String] URL of the server endpoint that returns a filtered `<tree-view>` HTML fragment. When set, activates async (server-side) filtering mode. See "Async loading strategy" above.
192+
# @param show_search_highlighting [Boolean] Only relevant in async mode (`src:` must be set). When `true` (default), the client highlights matching text using the CSS Custom Highlight API or `<mark>` elements. When `false`, the client skips highlighting entirely; the server is responsible for including highlight markup (e.g. `<mark>` tags) in the returned HTML fragment.
192193
# @param tree_view_arguments [Hash] Arguments that will be passed to the underlying <%= link_to_component(Primer::Alpha::TreeView) %> component.
193194
# @param form_arguments [Hash] Form arguments that will be passed to the underlying <%= link_to_component(Primer::Alpha::TreeView) %> component. These arguments allow the selections made within a `FilterableTreeView` to be submitted to the server as part of a Rails form. Pass the `builder:` and `name:` options to this hash. `builder:` should be an instance of `ActionView::Helpers::FormBuilder`, which are created by the standard Rails `#form_with` and `#form_for` helpers. The `name:` option is the desired name of the field that will be included in the params sent to the server on form submission.
194195
# @param filter_input_arguments [Hash] Arguments that will be passed to the <%= link_to_component(Primer::Alpha::TextField) %> component.
@@ -197,6 +198,7 @@ class FilterableTreeView < Primer::Component
197198
# @param no_results_node_arguments [Hash] Arguments that will be passed to a <%= link_to_component(Primer::Alpha::TreeView::LeafNode) %> component that appears when no items match the filter criteria.
198199
def initialize(
199200
src: nil,
201+
show_search_highlighting: true,
200202
tree_view_arguments: {},
201203
form_arguments: {},
202204
filter_input_arguments: {},
@@ -250,6 +252,11 @@ def initialize(
250252
@system_arguments = deny_tag_argument(**system_arguments)
251253
@system_arguments[:tag] = :"filterable-tree-view"
252254
@system_arguments[:src] = src if src
255+
@system_arguments[:data] = merge_data(
256+
@system_arguments, {
257+
data: { show_search_highlighting: false }
258+
}
259+
) unless show_search_highlighting
253260

254261
@no_results_node_arguments = no_results_node_arguments.reverse_merge(DEFAULT_NO_RESULTS_NODE_ARGUMENTS)
255262
end

app/components/primer/open_project/filterable_tree_view.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export class FilterableTreeViewElement extends HTMLElement {
6969
return !!this.#src
7070
}
7171

72+
get #clientHighlightsEnabled(): boolean {
73+
return this.getAttribute('data-show-search-highlighting') !== 'false'
74+
}
75+
7276
handleEvent(event: Event) {
7377
if (event.target === this.filterModeControl) {
7478
this.#handleFilterModeEvent(event)
@@ -372,12 +376,12 @@ export class FilterableTreeViewElement extends HTMLElement {
372376

373377
if (requestWasFiltered) {
374378
this.#expandAllSubTrees()
375-
this.#applyAsyncHighlights(query)
379+
if (this.#clientHighlightsEnabled) this.#applyAsyncHighlights(query)
376380
const hasResults = !!this.treeViewList?.querySelector('[role=treeitem]')
377381
this.noResultsMessage.toggleAttribute('hidden', hasResults)
378382
this.treeViewList?.toggleAttribute('hidden', !hasResults)
379383
} else {
380-
this.#removeHighlights()
384+
if (this.#clientHighlightsEnabled) this.#removeHighlights()
381385
this.#restoreExpansionState()
382386
this.noResultsMessage.setAttribute('hidden', 'hidden')
383387
this.treeViewList?.removeAttribute('hidden')

app/controllers/primer/view_components/filterable_tree_view_items_controller.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,14 @@ def self.branch(id:, label:, children:)
165165
def index
166166
query = params[:query].to_s.strip
167167
select_variant = (params[:select_variant].presence || "multiple").to_sym
168+
server_highlights = params[:server_highlights] == "true"
168169
nodes = TREE.filter_map { |node| node.filter(query) }
169170

170171
render locals: {
171172
nodes: nodes,
172173
query: query,
173-
select_variant: select_variant
174+
select_variant: select_variant,
175+
server_highlights: server_highlights
174176
}
175177
end
176178

app/views/primer/view_components/filterable_tree_view_items/_node.html.erb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1+
<%
2+
display_label = if local_assigns[:server_highlights] && query.present?
3+
idx = node.label.downcase.index(query.downcase)
4+
if idx
5+
(h(node.label[0...idx]) +
6+
content_tag(:mark, node.label[idx...idx + query.length]) +
7+
h(node.label[idx + query.length..])).html_safe
8+
else
9+
node.label
10+
end
11+
else
12+
node.label
13+
end
14+
%>
115
<% if node.leaf? %>
216
<% component.with_leaf(
3-
label: node.label,
17+
label: display_label,
418
select_variant: select_variant,
519
data: { node_id: node.id }
620
) %>
@@ -22,7 +36,7 @@
2236
hierarchy_only = query.present? && !node.label.downcase.include?(query.downcase)
2337
%>
2438
<% component.with_sub_tree(
25-
label: node.label,
39+
label: display_label,
2640
select_variant: select_variant,
2741
**sub_tree_opts,
2842
data: {
@@ -32,7 +46,7 @@
3246
) do |sub| %>
3347
<% node.children.each do |child| %>
3448
<%= render partial: "primer/view_components/filterable_tree_view_items/node",
35-
locals: { component: sub, node: child, query: query, select_variant: select_variant } %>
49+
locals: { component: sub, node: child, query: query, select_variant: select_variant, server_highlights: local_assigns[:server_highlights] } %>
3650
<% end %>
3751
<% end %>
3852
<% end %>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<%= render(Primer::Alpha::TreeView.new(data: { target: "filterable-tree-view.treeViewList" })) do |tree| %>
22
<% nodes.each do |node| %>
33
<%= render partial: "primer/view_components/filterable_tree_view_items/node",
4-
locals: { component: tree, node: node, query: query, select_variant: select_variant } %>
4+
locals: { component: tree, node: node, query: query, select_variant: select_variant, server_highlights: server_highlights } %>
55
<% end %>
66
<% end %>

previews/primer/open_project/filterable_tree_view_preview.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ def async(select_variant: :single)
123123
})
124124
end
125125

126+
# @label Async (with server-side highlighting)
127+
#
128+
# Demonstrates `show_search_highlighting: false`: the client skips its own highlight logic
129+
# and the server is responsible for wrapping matching text in `<mark>` (or other HTML) elements.
130+
#
131+
# @param select_variant [Symbol] select [multiple, single, none]
132+
def async_server_highlights(select_variant: :single)
133+
render_with_template(locals: {
134+
select_variant: select_variant.to_sym
135+
})
136+
end
137+
126138
# @label Async form input
127139
def async_form_input
128140
render_with_template
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<%= render(Primer::OpenProject::FilterableTreeView.new(
2+
show_search_highlighting: false,
3+
src: primer_view_components.filterable_tree_view_items_tree_path(select_variant: select_variant, server_highlights: true)
4+
)) %>

test/components/primer/open_project/filterable_tree_view_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,26 @@ def test_custom_filter_modes
274274
assert_selector "segmented-control li", text: "Foo"
275275
end
276276

277+
# ─── show_search_highlighting ──────────────────────────────────────────
278+
279+
def test_show_search_highlighting_true_by_default_sets_no_data_attribute
280+
render_inline(Primer::OpenProject::FilterableTreeView.new)
281+
282+
assert_selector "filterable-tree-view:not([data-show-search-highlighting])"
283+
end
284+
285+
def test_show_search_highlighting_false_sets_data_attribute
286+
render_inline(Primer::OpenProject::FilterableTreeView.new(show_search_highlighting: false))
287+
288+
assert_selector "filterable-tree-view[data-show-search-highlighting='false']"
289+
end
290+
291+
def test_show_search_highlighting_true_sets_no_data_attribute
292+
render_inline(Primer::OpenProject::FilterableTreeView.new(show_search_highlighting: true))
293+
294+
assert_selector "filterable-tree-view:not([data-show-search-highlighting])"
295+
end
296+
277297
# ─── Async mode ────────────────────────────────────────────────────────
278298

279299
def test_src_attribute_is_set_when_src_is_provided

0 commit comments

Comments
 (0)