Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5b576ed
Skeleton for action-list web component
camertron Feb 15, 2024
07760ca
add typscript functionailty
thesnowrose Feb 15, 2024
02d5b0c
It works
camertron Feb 15, 2024
5a94267
Add :truncate option (same as true); add previews
camertron Feb 15, 2024
a8b856c
add action list test
thesnowrose Feb 20, 2024
8b4a175
undo package-lock changes, add new line for linter
thesnowrose Feb 20, 2024
4e4f60f
add constants to json file, address errors
thesnowrose Feb 20, 2024
501b598
Fix show more item
camertron Feb 20, 2024
a572c3c
fix constant error
thesnowrose Feb 21, 2024
6021344
Generating component snapshots
thesnowrose Feb 21, 2024
24a1016
add component specific tests
thesnowrose Feb 21, 2024
d7cdb81
Merge remote-tracking branch 'refs/remotes/origin/nav_list_tooltips_o…
thesnowrose Feb 21, 2024
80d039b
linter
thesnowrose Feb 21, 2024
f3e3e2e
cleanup uncommented out code, remove constants from json
thesnowrose Feb 21, 2024
a0815ac
Add changeset
thesnowrose Feb 21, 2024
1ea6c9d
update navlist html to load action list assets
thesnowrose Feb 21, 2024
00e6634
Modify NavListElement and NavListGroupElement to inherit from ActionL…
camertron Feb 21, 2024
2ccdd33
Test for visible tooltip when window shrinks
camertron Feb 21, 2024
fe6d06a
Fix CSS selectors test
camertron Feb 21, 2024
70228c1
Pr feedback
thesnowrose Feb 23, 2024
2b215ee
Merge branch 'main' into nav_list_tooltips_on_truncation
thesnowrose Feb 23, 2024
200911a
Generating component snapshots
thesnowrose Feb 23, 2024
37e45c8
Build
camertron Feb 23, 2024
e3cbaf1
convert ActionListElement to composition class
thesnowrose Feb 27, 2024
5fc53fb
Merge remote-tracking branch 'refs/remotes/origin/nav_list_tooltips_o…
thesnowrose Feb 27, 2024
d9b36ed
Merge branch 'main' into nav_list_tooltips_on_truncation
camertron Feb 27, 2024
e5db6f9
Generating component snapshots
camertron Feb 27, 2024
5f0efc4
Build
camertron Feb 27, 2024
c0dcbd0
Re-introduce <action-list> custom element; ensure top-level NavList i…
camertron Feb 27, 2024
cccae91
Generating component snapshots
camertron Feb 27, 2024
8e61e91
Merge upstream
camertron Feb 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lucky-phones-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/view-components": minor
---

Add the ability to show a tooltip for navlists on text truncation
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 19 additions & 17 deletions app/components/primer/alpha/action_list.html.erb
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
<%= render(Primer::BaseComponent.new(tag: :div)) do %>
<% if acts_as_form_input? %>
<span data-list-inputs="true">
<%= @form_builder.hidden_field(@input_name, multiple: multi_select?, skip_default_ids: true) %>
</span>
<% end %>
<% if heading %>
<%= heading %>
<% end %>
<%= render(Primer::BaseComponent.new(tag: :ul, **@system_arguments)) do %>
<% items.each_with_index do |item, index| %>
<%# the conditions here make sure two dividers are never rendered one after the other %>
<% if index > 0 && @show_dividers && !item.is_a?(Divider) && !items[index - 1].is_a?(Divider) %>
<%= render(Primer::Alpha::ActionList::Divider.new) %>
<action-list>
<%= render(Primer::BaseComponent.new(tag: :div)) do %>
<% if acts_as_form_input? %>
<span data-list-inputs="true">
<%= @form_builder.hidden_field(@input_name, multiple: multi_select?, skip_default_ids: true) %>
</span>
<% end %>
<% if heading %>
<%= heading %>
<% end %>
<%= render(Primer::BaseComponent.new(tag: :ul, **@system_arguments)) do %>
<% items.each_with_index do |item, index| %>
<%# the conditions here make sure two dividers are never rendered one after the other %>
<% if index > 0 && @show_dividers && !item.is_a?(Divider) && !items[index - 1].is_a?(Divider) %>
<%= render(Primer::Alpha::ActionList::Divider.new) %>
<% end %>
<%= item %>
<% end %>
<%= item %>
<% end %>
<%= capture(&post_list_content_block) if post_list_content_block %>
<% end %>
<%= capture(&post_list_content_block) if post_list_content_block %>
<% end %>
</action-list>
8 changes: 8 additions & 0 deletions app/components/primer/alpha/action_list.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

/* ActionList */

action-list {
display: block;
}

nav-list {
display: block;
}

.ActionListHeader {
margin-left: var(--base-size-8);
margin-bottom: var(--base-size-16);
Expand Down
58 changes: 58 additions & 0 deletions app/components/primer/alpha/action_list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {controller} from '@github/catalyst'

// eslint-disable-next-line custom-elements/no-exports-with-element
export class ActionListTruncationObserver {
resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const action = entry.target

if (action instanceof HTMLElement) {
this.update(action)
}
}
})

constructor(el: HTMLElement) {
this.resizeObserver.observe(el)
}

unobserve(el: HTMLElement) {
this.resizeObserver.unobserve(el)
}

update(el: HTMLElement) {
const label = el.querySelector('.ActionListItem-label')
if (!label) return

const tooltip = el.querySelector('.ActionListItem-truncationTooltip') as HTMLElement | null
if (!tooltip) return

const isTruncated = label.scrollWidth > label.clientWidth

if (isTruncated) {
tooltip.style.display = ''
} else {
tooltip.style.display = 'none'
}
}
}

@controller
// eslint-disable-next-line custom-elements/expose-class-on-global
export class ActionListElement extends HTMLElement {
#truncationObserver: ActionListTruncationObserver

connectedCallback() {
this.#truncationObserver = new ActionListTruncationObserver(this)
}

disconnectedCallback() {
this.#truncationObserver.unobserve(this)
}
}

declare global {
interface Window {
ActionListElement: typeof ActionListElement
}
}
25 changes: 19 additions & 6 deletions app/components/primer/alpha/action_list/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ class Item < Primer::Component
}.freeze
SCHEME_OPTIONS = SCHEME_MAPPINGS.keys.freeze

DEFAULT_TRUNCATION_BEHAVIOR = :none
TRUNCATION_BEHAVIOR_MAPPINGS = {
DEFAULT_TRUNCATION_BEHAVIOR => nil,
false => nil,
:show_tooltip => "ActionListItem-label--truncate",
:truncate => "ActionListItem-label--truncate",
true => "ActionListItem-label--truncate"
}
TRUNCATION_BEHAVIOR_OPTIONS = TRUNCATION_BEHAVIOR_MAPPINGS.keys.freeze

# Description content that complements the item's label. See `ActionList`'s `description_scheme` argument
# for layout options.
renders_one :description
Expand Down Expand Up @@ -120,6 +130,8 @@ class Item < Primer::Component
system_arguments[:for_id] = @id
system_arguments[:type] ||= :description

system_arguments[:classes] = class_names(system_arguments[:classes], "ActionListItem-truncationTooltip") if @truncate_label == :show_tooltip

Primer::Alpha::Tooltip.new(**system_arguments)
}

Expand Down Expand Up @@ -148,7 +160,7 @@ class Item < Primer::Component
# @param label_arguments [Hash] <%= link_to_system_arguments_docs %> used to construct the label.
# @param content_arguments [Hash] <%= link_to_system_arguments_docs %> used to construct the item's anchor or button tag.
# @param form_arguments [Hash] Allows the item to submit a form on click. The URL passed in the `href:` option will be used as the form action. Pass the `method:` option to this hash to control what kind of request is made, <%= one_of(Primer::Alpha::ActionList::FormWrapper::HTTP_METHOD_OPTIONS) %> The `name:` option is required and specifies the desired name of the field that will be included in the params sent to the server on form submission. Specify the `value:` option to send a custom value to the server; otherwise the value of `name:` is sent.
# @param truncate_label [Boolean] Truncate label with ellipsis.
# @param truncate_label [Boolean | Symbol] How the label should be truncated when the text does not fit inside the bounds of the list item. <%= one_of(Primer::Alpha::ActionList::Item::TRUNCATION_BEHAVIOR_OPTIONS) %> Pass `false` or `:none` to wrap label text. Pass `true` or `:truncate` to truncate labels with ellipses. Pass `:show_tooltip` to show the entire label contents in a tooltip when the item is hovered.
# @param href [String] Link URL.
# @param role [String] ARIA role describing the function of the item.
# @param size [Symbol] Controls block sizing of the item.
Expand All @@ -168,7 +180,7 @@ def initialize(
content_arguments: {},
form_arguments: {},
parent: nil,
truncate_label: false,
truncate_label: :none,
href: nil,
role: nil,
size: DEFAULT_SIZE,
Expand Down Expand Up @@ -206,9 +218,6 @@ def initialize(
"ActionListItem--disabled" => @disabled
)

@system_arguments[:data] ||= {}
@system_arguments[:data][:targets] = "#{list_class.custom_element_name}.items"

@system_arguments[:data] = merge_data(
@system_arguments, {
data: {
Expand All @@ -224,7 +233,7 @@ def initialize(
label_classes,
label_arguments[:classes],
"ActionListItem-label",
"ActionListItem-label--truncate" => @truncate_label
TRUNCATION_BEHAVIOR_MAPPINGS[@truncate_label],
)
}

Expand Down Expand Up @@ -293,6 +302,10 @@ def before_render
"ActionListItem--withActions" => trailing_action.present?
)

if @truncate_label == :show_tooltip && !tooltip?
with_tooltip(text: @label)
end

return unless leading_visual

@content_arguments[:classes] = class_names(
Expand Down
2 changes: 1 addition & 1 deletion app/components/primer/beta/nav_list.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<%= render(Primer::BaseComponent.new(tag: :nav, **@system_arguments)) do %>
<%= heading %>
<nav-list>
<%= render(Primer::ConditionalWrapper.new(condition: render_outer_list?, tag: :ul, classes: "ActionListWrap")) do %>
<%= render(Primer::ConditionalWrapper.new(condition: render_outer_list?, tag: :ul, classes: "ActionListWrap", data: { target: "nav-list.topLevelList" })) do %>
<% items.each_with_index do |item, index| %>
<% if index > 0 && render_divider_between?(item, items[index - 1]) %>
<%= render(Primer::Alpha::ActionList::Divider.new) %>
Expand Down
19 changes: 18 additions & 1 deletion app/components/primer/beta/nav_list.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
/* eslint-disable custom-elements/expose-class-on-global */
import {controller, targets} from '@github/catalyst'
import {controller, target, targets} from '@github/catalyst'
import {ActionListTruncationObserver} from '../alpha/action_list'

@controller
export class NavListElement extends HTMLElement {
@targets items: HTMLElement[]
@target topLevelList: HTMLElement

#truncationObserver: ActionListTruncationObserver

connectedCallback() {
// groups are wrapped in <action-list>, which handles resizing on its own
if (this.topLevelList) {
this.#truncationObserver = new ActionListTruncationObserver(this.topLevelList)
}
}

disconnectedCallback() {
if (this.topLevelList) {
this.#truncationObserver.unobserve(this.topLevelList)
}
}

selectItemById(itemId: string | null): boolean {
if (!itemId) return false
Expand Down
3 changes: 3 additions & 0 deletions app/components/primer/beta/nav_list_group_element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {controller, target, targets} from '@github/catalyst'
import {ActionListTruncationObserver} from '../alpha/action_list'

@controller
export class NavListGroupElement extends HTMLElement {
Expand Down Expand Up @@ -86,6 +87,8 @@ export class NavListGroupElement extends HTMLElement {
template.innerHTML = html
return document.importNode(template.content, true)
}

#truncateObserver = new ActionListTruncationObserver(this)
}

declare global {
Expand Down
1 change: 1 addition & 0 deletions app/components/primer/primer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '@github/include-fragment-element'
import './alpha/action_list'
import './alpha/action_bar_element'
import './alpha/dropdown'
import './anchored_position'
Expand Down
42 changes: 42 additions & 0 deletions previews/primer/alpha/action_list_preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,48 @@ def item_active
component.with_item(label: "Active item", href: "/", active: true)
end
end

# @label Long label
#
# @param truncate_label [Symbol] select [none, truncate, show_tooltip]
# @snapshot
Comment thread
thesnowrose marked this conversation as resolved.
def long_label_with_tooltip(truncate_label: :show_tooltip)
render(Primer::Alpha::ActionList.new(aria: { label: "List heading" })) do |component|
component.with_item(
label: "Really really long label that may wrap, truncate, or appear as a tooltip",
truncate_label: truncate_label
)
end
end

def long_label_wrap(truncate_label: :none)
render(Primer::Alpha::ActionList.new(aria: { label: "List heading" })) do |component|
component.with_item(
label: "Really really long label that may wrap, truncate, or appear as a tooltip",
truncate_label: truncate_label
)
end
end

def long_label_truncate_no_tooltip(truncate_label: :truncate)
render(Primer::Alpha::ActionList.new(aria: { label: "List heading" })) do |component|
component.with_item(
label: "Really really long label that may wrap, truncate, or appear as a tooltip",
truncate_label: truncate_label
)
end
end

def long_label_show_tooltip_with_truncate_label(truncate_label: :none)
render(Primer::Alpha::ActionList.new(aria: { label: "List heading" })) do |component|
component.with_item(
label: "Really really long label that may wrap, truncate, or appear as a tooltip",
truncate_label: truncate_label
) do |item|
item.with_tooltip(text: "this is a tooltip")
end
end
end
end
end
end
43 changes: 43 additions & 0 deletions previews/primer/beta/nav_list_preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,49 @@ def show_more_item
# @label Trailing action
# @snapshot
def trailing_action; end

# @label Long label truncate overflow
#
# @param truncate_label [Symbol] select [none, truncate, show_tooltip]
# @snapshot
Comment thread
thesnowrose marked this conversation as resolved.
def long_label_with_tooltip(truncate_label: :show_tooltip)
render(Primer::Beta::NavList.new(aria: { label: "List heading" })) do |component|
component.with_item(
label: "Really really long label that may wrap, truncate, or appear as a tooltip",
truncate_label: truncate_label
) do |item|
item.with_trailing_visual_icon(icon: :plus)
end
end
end

def long_label_wrap(truncate_label: :none)
render(Primer::Beta::NavList.new(aria: { label: "List heading" })) do |component|
component.with_item(
label: "Really really long label that may wrap, truncate, or appear as a tooltip",
truncate_label: truncate_label
)
end
end

def long_label_truncate_no_tooltip(truncate_label: :truncate)
render(Primer::Beta::NavList.new(aria: { label: "List heading" })) do |component|
component.with_item(
label: "Really really long label that may wrap, truncate, or appear as a tooltip",
truncate_label: truncate_label
)
end
end

def long_label_show_tooltip_no_truncate_label
Comment thread
thesnowrose marked this conversation as resolved.
render(Primer::Beta::NavList.new(aria: { label: "List heading" })) do |component|
component.with_item(
label: "Really really long label that may wrap, truncate, or appear as a tooltip",
) do |item|
item.with_tooltip(text: "this is a tooltip")
end
end
end
end
end
end
18 changes: 18 additions & 0 deletions test/components/alpha/action_list_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ def test_item_with_leading_icon
assert_selector(".octicon-arrow-down")
end

def test_item_text_truncates_with_tooltip
render_inline(Primer::Alpha::ActionList.new(aria: { label: "List" })) do |component|
component.with_item(label: "Item 1", href: "/item1", truncate_label: :show_tooltip)
end

assert_selector "li.ActionListItem", text: "Item 1"
assert_selector "li.ActionListItem span.ActionListItem-label--truncate"
end

def test_item_text_wraps
render_inline(Primer::Alpha::ActionList.new(aria: { label: "List" })) do |component|
component.with_item(label: "Item 1", href: "/item1", truncate_label: :none)
end

assert_selector "li.ActionListItem", text: "Item 1"
assert_selector "li.ActionListItem span.ActionListItem-label"
end

def test_list_labelled_by_heading
render_preview(:default)

Expand Down
3 changes: 2 additions & 1 deletion test/css/component_specific_selectors_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class ComponentSpecificSelectorsTest < Minitest::Test
".ActionListItem.ActionListItem--hasSubItem>.ActionListContent",
".ActionListItem--subItem>.ActionListContent>.ActionListItem-label",
".ActionListHeader", # used exclusively by NavList
".ActionListItem.ActionListItem--navActive" # used exclusively by NavList
".ActionListItem.ActionListItem--navActive", # used exclusively by NavList
"nav-list" # used exclusively by NavList
],
Primer::Alpha::AutoComplete => [
".autocomplete-item"
Expand Down
Loading