From a54e5510ecc87b55ed9a6bf71ac1c9d2e3b3fef9 Mon Sep 17 00:00:00 2001 From: Katie Langerman Date: Tue, 30 Aug 2022 17:02:45 -0700 Subject: [PATCH] Beta::Button and Beta::IconButton visual refinements (#1325) * setup previews + new markup * add params * setup icon button preview * add tooltip from main * add tooltip * fix previews, pull main * Moving IconButton and Button Co-authored-by: Katie Langerman * Adding test and css for Button * Adding css and test files for IconButton * Moving button css to pvc * Convert tabs to spaces * Removing box param * A bunch of changes Co-authored-by: Katie Langerman * Revert css chages * Basic render test for IconButton * Updating component with args * Adding content to render test * Documenting update for button * Updating to use Primer::Beta::Button::SIZE * docs: build docs * Remove docs css class generating test * Also removing the array since we're not using it * Remove old arguments * Create flat-plums-suffer.md Co-authored-by: Jon Rohan Co-authored-by: Katie Langerman Co-authored-by: Actions Auto Build --- .changeset/flat-plums-suffer.md | 5 + app/components/primer/beta/button.html.erb | 23 ++ app/components/primer/beta/button.pcss | 332 ++++++++++++++++++ app/components/primer/beta/button.rb | 189 ++++++++++ .../primer/beta/icon_button.html.erb | 6 + app/components/primer/beta/icon_button.rb | 104 ++++++ app/components/primer/primer.pcss | 1 + lib/tasks/docs.rake | 10 +- static/arguments.yml | 74 ++++ static/audited_at.json | 2 + static/classes.yml | 230 ------------ static/constants.json | 55 +++ static/statuses.json | 2 + test/components/component_test.rb | 2 + test/components/primer/beta/button_test.rb | 13 + .../primer/beta/icon_button_test.rb | 16 + test/lib/css_coverage_test.rb | 13 - test/previews/primer/beta/button_preview.rb | 87 +++++ .../beta/button_preview/with_visuals.html.erb | 12 + .../primer/beta/icon_button_preview.rb | 37 ++ 20 files changed, 962 insertions(+), 251 deletions(-) create mode 100644 .changeset/flat-plums-suffer.md create mode 100644 app/components/primer/beta/button.html.erb create mode 100644 app/components/primer/beta/button.pcss create mode 100644 app/components/primer/beta/button.rb create mode 100644 app/components/primer/beta/icon_button.html.erb create mode 100644 app/components/primer/beta/icon_button.rb delete mode 100644 static/classes.yml create mode 100644 test/components/primer/beta/button_test.rb create mode 100644 test/components/primer/beta/icon_button_test.rb create mode 100644 test/previews/primer/beta/button_preview.rb create mode 100644 test/previews/primer/beta/button_preview/with_visuals.html.erb create mode 100644 test/previews/primer/beta/icon_button_preview.rb diff --git a/.changeset/flat-plums-suffer.md b/.changeset/flat-plums-suffer.md new file mode 100644 index 0000000000..61068028c7 --- /dev/null +++ b/.changeset/flat-plums-suffer.md @@ -0,0 +1,5 @@ +--- +"@primer/view-components": patch +--- + +Adding Primer::Beta::Button and Primer::Beta::IconButton with visual refinements diff --git a/app/components/primer/beta/button.html.erb b/app/components/primer/beta/button.html.erb new file mode 100644 index 0000000000..78d23c6a61 --- /dev/null +++ b/app/components/primer/beta/button.html.erb @@ -0,0 +1,23 @@ +<%= render Primer::ConditionalWrapper.new(condition: tooltip.present?, tag: :div, classes: "Button-withTooltip") do -%> + <%= render Primer::Beta::BaseButton.new(**@system_arguments) do -%> + + <% if leading_visual %> + + <%= leading_visual %> + + <% end %> + <%= trimmed_content %> + <% if trailing_visual %> + + <%= trailing_visual %> + + <% end %> + + <% if trailing_action %> + + <%= trailing_action %> + + <% end %> + <%= tooltip %> + <% end -%> +<% end -%> diff --git a/app/components/primer/beta/button.pcss b/app/components/primer/beta/button.pcss new file mode 100644 index 0000000000..b767932906 --- /dev/null +++ b/app/components/primer/beta/button.pcss @@ -0,0 +1,332 @@ +/* CSS for Button */ +/* temporary, pre primitives release */ +:root { + --primer-duration-fast: 80ms; + --primer-easing-easeInOut: cubic-bezier(0.65, 0, 0.35, 1); +} + +/* base button */ +.Button { + position: relative; + font-size: var(--primer-text-body-size-medium, 14px); + font-weight: var(--base-text-weight-medium, 500); + cursor: pointer; + user-select: none; + background-color: transparent; + border: var(--primer-borderWidth-thin, 1px) solid; + border-color: transparent; + border-radius: var(--primer-borderRadius-medium, 6px); + color: var(--color-btn-text); + transition: var(--primer-duration-fast) var(--primer-easing-easeInOut); + transition-property: color, fill, background-color, border-color; + text-align: center; + height: var(--primer-control-medium-size, 32px); + padding: 0 var(--primer-control-medium-paddingInline-normal, 12px); + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: var(--primer-control-medium-gap, 8px); + + /* mobile friendly sizing */ + @media (pointer: course) { + &::before { + @mixin minTouchTarget 48px, 48px; + } + } + + /* base states */ + + &:hover { + transition-duration: var(--primer-duration-fast); + } + + &:active, + &.Button--active { + transition: none; + } + + &:disabled, + &.Button--disabled, + &[aria-disabled='true'] { + cursor: not-allowed; + box-shadow: none; + } + + /* &:focus { + @mixin focusOutline; + } */ +} + +.Button-withTooltip { + position: relative; + display: inline-block; +} + +a.Button, +summary.Button { + display: inline-flex; + + &:hover { + text-decoration: none; + } +} + +/* wrap grid content to allow trailingAction to lock-right */ +.Button-content { + flex: 1 0 auto; + display: grid; + grid-template-areas: 'leadingVisual text trailingVisual'; + grid-template-columns: min-content minmax(0, auto) min-content; + align-items: center; + place-content: center; + /* padding-bottom: 1px; optical alignment for firefox */ + + & > :not(:last-child) { + margin-right: var(--primer-control-medium-gap, 8px); + } +} + +/* center child elements for fullWidth */ +.Button-content--alignStart { + justify-content: start; +} + +/* button child elements */ + +/* align svg */ +.Button-visual { + display: flex; + pointer-events: none; /* allow click handler to work, avoiding visuals */ +} + +.Button-label { + grid-area: text; + white-space: nowrap; + line-height: var(--primer-text-body-lineHeight-medium, calc(20/14)); +} + +.Button-leadingVisual { + grid-area: leadingVisual; +} + +.Button-trailingVisual { + grid-area: trailingVisual; +} + +.Button-trailingAction { + margin-right: calc(var(--base-size-4, 4px) * -1); +} + +/* sizes */ + +.Button--small { + font-size: var(--primer-text-body-size-small, 12px); + height: var(--primer-control-small-size, 28px); + padding: 0 var(--primer-control-small-paddingInline-normal, 12px); + gap: var(--primer-control-small-gap, 4px); + + .Button-label { + line-height: var(--primer-text-body-lineHeight-small, calc(20/12)); + } + + .Button-content { + & > :not(:last-child) { + margin-right: var(--primer-control-small-gap, 4px); + } + } +} + +.Button--large { + height: var(--primer-control-large-size, 40px); + padding: 0 var(--primer-control-large-paddingInline-normal, 12px); + gap: var(--primer-control-large-gap, 8px); + + .Button-label { + line-height: var(--primer-text-body-lineHeight-large, calc(48/32)); + } + + .Button-content { + & > :not(:last-child) { + margin-right: var(--primer-control-large-gap, 8px); + } + } +} + +.Button--fullWidth { + width: 100%; +} + +/* variants */ + +/* primary */ +.Button--primary { + color: var(--color-btn-primary-text); + fill: var(--color-btn-primary-icon); + background-color: var(--color-btn-primary-bg); + border-color: var(--color-btn-primary-border); + box-shadow: var(--color-btn-primary-shadow), var(--color-btn-primary-inset-shadow); + + &:hover { + background-color: var(--color-btn-primary-hover-bg); + border-color: var(--color-btn-primary-hover-border); + } + + /* fallback :focus state */ + &:focus { + @mixin focusOutlineOnEmphasis; + + /* remove fallback :focus if :focus-visible is supported */ + &:not(:focus-visible) { + outline: solid 1px transparent; + box-shadow: none; + } + } + + /* default focus state */ + &:focus-visible { + @mixin focusOutlineOnEmphasis; + } + + &:active, + &[aria-pressed='true'], + &.Button--pressed { + background-color: var(--color-btn-primary-selected-bg); + box-shadow: var(--color-btn-primary-selected-shadow); + } + + &:disabled, + &.Button--disabled, + &[aria-disabled='true'] { + color: var(--color-btn-primary-disabled-text); + background-color: var(--color-btn-primary-disabled-bg); + border-color: var(--color-btn-primary-disabled-border); + fill: var(--color-btn-primary-disabled-text); + } +} + +/* default (secondary) */ +.Button--secondary { + color: var(--color-btn-text); + fill: var(--color-fg-muted); /* help this */ + background-color: var(--color-btn-bg); + border-color: var(--color-btn-border); + box-shadow: var(--color-btn-shadow), var(--color-btn-inset-shadow); + + &:hover { + background-color: var(--color-btn-hover-bg); + border-color: var(--color-btn-hover-border); + } + + &:active, + &.Button--active { + background-color: var(--color-btn-active-bg); + border-color: var(--color-btn-active-border); + } + + &[aria-pressed='true'], + &.Button--pressed { + background-color: var(--color-btn-selected-bg); + box-shadow: var(--color-primer-shadow-inset); + } + + &:disabled, + &.Button--disabled, + &[aria-disabled='true'] { + color: var(--color-primer-fg-disabled); + background-color: var(--color-btn-bg); + border-color: var(--color-btn-border); + fill: var(--color-primer-fg-disabled); + } +} + +/* link color without svg */ +.Button--invisible { + color: var(--color-fg-default); + fill: var(--color-fg-default); + border: none; + + &:hover { + background-color: var(--color-action-list-item-default-hover-bg); + } + + &[aria-pressed='true'], + &:active, + &.Button--active, + &.Button--pressed { + background-color: var(--color-action-list-item-default-active-bg); + /* box-shadow: var(--color-primer-shadow-inset); */ + } + + &:disabled, + &.Button--disabled, + &[aria-disabled='true'] { + color: var(--color-primer-fg-disabled); + background-color: var(--color-btn-bg); + border-color: var(--color-btn-border); + fill: var(--color-primer-fg-disabled); + } + + /* if visual is present, muted label color */ + .Button-label:not(:only-child) { + color: var(--color-btn-text); + } + + /* if trailingAction is present, muted label color */ + .Button-content:not(:only-child) { + .Button-label { + color: var(--color-btn-text); + } + } +} + +/* danger */ +.Button--danger { + color: var(--color-btn-danger-text); + fill: var(--color-btn-danger-icon); + background-color: var(--color-btn-bg); + border-color: var(--color-btn-border); + box-shadow: var(--color-btn-shadow), var(--color-btn-inset-shadow); + + &:hover { + color: var(--color-btn-danger-hover-text); + fill: var(--color-btn-danger-hover-text); + background-color: var(--color-btn-danger-hover-bg); + border-color: var(--color-btn-danger-hover-border); + box-shadow: var(--color-btn-danger-hover-shadow), var(--color-btn-danger-hover-inset-shadow); + } + + &:active, + &[aria-pressed='true'], + &.Button--pressed { + color: var(--color-btn-danger-selected-text); + fill: var(--color-btn-danger-selected-text); + background-color: var(--color-btn-danger-selected-bg); + border-color: var(--color-btn-danger-selected-border); + box-shadow: var(--color-btn-danger-selected-shadow); + } + + &:disabled, + &.disabled, + &[aria-disabled='true'] { + color: var(--color-btn-danger-disabled-text); + fill: var(--color-btn-danger-disabled-text); + background-color: var(--color-btn-danger-disabled-bg); + border-color: var(--color-btn-border); + } +} + +.Button--iconOnly { + display: grid; + place-content: center; + padding: unset; + width: var(--primer-control-medium-size, 32px); + + &.Button--small { + width: var(--primer-control-small-size, 28px); + } + + &.Button--large { + width: var(--primer-control-large-size, 40px); + } +} diff --git a/app/components/primer/beta/button.rb b/app/components/primer/beta/button.rb new file mode 100644 index 0000000000..027425b212 --- /dev/null +++ b/app/components/primer/beta/button.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +module Primer + module Beta + # Use `Button` for actions (e.g. in forms). Use links for destinations, or moving from one page to another. + class Button < Primer::Component + status :beta + + DEFAULT_SCHEME = :default + LINK_SCHEME = :link + SCHEME_MAPPINGS = { + DEFAULT_SCHEME => "", + :primary => "Button--primary", + :secondary => "Button--secondary", + :default => "Button--secondary", + :danger => "Button--danger", + :outline => "btn-outline", + :invisible => "Button--invisible", + LINK_SCHEME => "btn-link" + }.freeze + SCHEME_OPTIONS = SCHEME_MAPPINGS.keys + + DEFAULT_SIZE = :medium + SIZE_MAPPINGS = { + :small => "Button--small", + :medium => "Button--medium", + :large => "Button--large", + DEFAULT_SIZE => "Button--medium" + }.freeze + SIZE_OPTIONS = SIZE_MAPPINGS.keys + + DEFAULT_ALIGN_CONTENT = :center + ALIGN_CONTENT_MAPPINGS = { + :start => "Button-content--alignStart", + :center => "", + DEFAULT_ALIGN_CONTENT => "" + }.freeze + ALIGN_CONTENT_OPTIONS = ALIGN_CONTENT_MAPPINGS.keys + + # Leading visuals appear to the left of the button text. + # + # Use: + # + # - `leading_visual_icon` for a <%= link_to_component(Primer::OcticonComponent) %>. + # + # @param system_arguments [Hash] Same arguments as <%= link_to_component(Primer::OcticonComponent) %>. + renders_one :leading_visual, types: { + icon: lambda { |**system_arguments| + Primer::OcticonComponent.new(**system_arguments) + } + } + + # Trailing visuals appear to the right of the button text. + # + # Use: + # + # - `trailing_visual_counter` for a <%= link_to_component(Primer::Beta::Counter) %>. + # + # @param system_arguments [Hash] Same arguments as <%= link_to_component(Primer::Beta::Counter) %>. + renders_one :trailing_visual, types: { + icon: Primer::OcticonComponent, + label: Primer::LabelComponent, + counter: Primer::CounterComponent + } + + # Trailing action appears to the right of the trailing visual. + # + # Use: + # + # - `trailing_action_icon` for a <%= link_to_component(Primer::OcticonComponent) %>. + # + # @param system_arguments [Hash] Same arguments as <%= link_to_component(Primer::OcticonComponent) %>. + renders_one :trailing_action, types: { + icon: Primer::OcticonComponent + } + + # `Tooltip` that appears on mouse hover or keyboard focus over the button. Use tooltips sparingly and as a last resort. + # **Important:** This tooltip defaults to `type: :description`. In a few scenarios, `type: :label` may be more appropriate. + # Consult the <%= link_to_component(Primer::Alpha::Tooltip) %> documentation for more information. + # + # @param type [Symbol] (:description) <%= one_of(Primer::Alpha::Tooltip::TYPE_OPTIONS) %> + # @param system_arguments [Hash] Same arguments as <%= link_to_component(Primer::Alpha::Tooltip) %>. + renders_one :tooltip, lambda { |**system_arguments| + raise ArgumentError, "Buttons with a tooltip must have a unique `id` set on the `Button`." if @id.blank? && !Rails.env.production? + + system_arguments[:for_id] = @id + system_arguments[:type] ||= :description + + Primer::Alpha::Tooltip.new(**system_arguments) + } + + # @example Schemes + # <%= render(Primer::Beta::Button.new) { "Default" } %> + # <%= render(Primer::Beta::Button.new(scheme: :primary)) { "Primary" } %> + # <%= render(Primer::Beta::Button.new(scheme: :danger)) { "Danger" } %> + # <%= render(Primer::Beta::Button.new(scheme: :outline)) { "Outline" } %> + # <%= render(Primer::Beta::Button.new(scheme: :invisible)) { "Invisible" } %> + # <%= render(Primer::Beta::Button.new(scheme: :link)) { "Link" } %> + # + # @example Sizes + # <%= render(Primer::Beta::Button.new(size: :small)) { "Small" } %> + # <%= render(Primer::Beta::Button.new(size: :medium)) { "Medium" } %> + # + # @example Block + # <%= render(Primer::Beta::Button.new(block: :true)) { "Block" } %> + # <%= render(Primer::Beta::Button.new(block: :true, scheme: :primary)) { "Primary block" } %> + # + # @example With leading visual + # <%= render(Primer::Beta::Button.new) do |c| %> + # <% c.with_leading_visual_icon(icon: :star) %> + # Button + # <% end %> + # + # @example With trailing visual + # <%= render(Primer::Beta::Button.new) do |c| %> + # <% c.with_trailing_visual_counter(count: 15) %> + # Button + # <% end %> + # + # @example With leading and trailing visuals + # <%= render(Primer::Beta::Button.new) do |c| %> + # <% c.with_leading_visual_icon(icon: :star) %> + # <% c.with_trailing_visual_counter(count: 15) %> + # Button + # <% end %> + # + # @example With tooltip + # @description + # Use tooltips sparingly and as a last resort. Consult the <%= link_to_component(Primer::Alpha::Tooltip) %> documentation for more information. + # @code + # <%= render(Primer::Beta::Button.new(id: "button-with-tooltip")) do |c| %> + # <% c.with_tooltip(text: "Tooltip text") %> + # Button + # <% end %> + # + # @param scheme [Symbol] <%= one_of(Primer::Beta::Button::SCHEME_OPTIONS) %> + # @param size [Symbol] <%= one_of(Primer::Beta::Button::SIZE_OPTIONS) %> + # @param full_width [Boolean] Whether button is full-width with `display: block`. + # @param align_content [Symbol] <%= one_of(Primer::Beta::Button::ALIGN_CONTENT_OPTIONS) %> + # @param tag [Symbol] (Primer::Beta::BaseButton::DEFAULT_TAG) <%= one_of(Primer::Beta::BaseButton::TAG_OPTIONS) %> + # @param type [Symbol] (Primer::Beta::BaseButton::DEFAULT_TYPE) <%= one_of(Primer::Beta::BaseButton::TYPE_OPTIONS) %> + # @param system_arguments [Hash] <%= link_to_system_arguments_docs %> + def initialize( + scheme: DEFAULT_SCHEME, + size: DEFAULT_SIZE, + full_width: false, + align_content: DEFAULT_ALIGN_CONTENT, + **system_arguments + ) + @scheme = scheme + + @system_arguments = system_arguments + + @id = @system_arguments[:id] + + @align_content_classes = class_names( + "Button-content", + system_arguments[:classes], + ALIGN_CONTENT_MAPPINGS[fetch_or_fallback(ALIGN_CONTENT_OPTIONS, align_content, DEFAULT_ALIGN_CONTENT)] + ) + + @system_arguments[:classes] = class_names( + system_arguments[:classes], + SCHEME_MAPPINGS[fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME)], + SIZE_MAPPINGS[fetch_or_fallback(SIZE_OPTIONS, size, DEFAULT_SIZE)], + "Button" => !link?, + "Button--fullWidth" => full_width + ) + end + + private + + def link? + @scheme == LINK_SCHEME + end + + def trimmed_content + return if content.blank? + + trimmed_content = content.strip + + return trimmed_content unless content.html_safe? + + # strip unsets `html_safe`, so we have to set it back again to guarantee that HTML blocks won't break + trimmed_content.html_safe # rubocop:disable Rails/OutputSafety + end + end + end +end diff --git a/app/components/primer/beta/icon_button.html.erb b/app/components/primer/beta/icon_button.html.erb new file mode 100644 index 0000000000..9259ab0eaa --- /dev/null +++ b/app/components/primer/beta/icon_button.html.erb @@ -0,0 +1,6 @@ +<%= render Primer::ConditionalWrapper.new(condition: render_tooltip?, tag: :div, classes: "Button-withTooltip") do %> + <%= render Primer::Beta::BaseButton.new(**@system_arguments) do -%> + <%= render Primer::OcticonComponent.new(icon: @icon, classes: "Button-visual") %> + <% end -%> + <%= render Primer::Alpha::Tooltip.new(**@tooltip_arguments) if render_tooltip? %> +<% end %> diff --git a/app/components/primer/beta/icon_button.rb b/app/components/primer/beta/icon_button.rb new file mode 100644 index 0000000000..8dd32e5e5b --- /dev/null +++ b/app/components/primer/beta/icon_button.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module Primer + module Beta + # Use `IconButton` to render Icon-only buttons without the default button styles. + # + # `IconButton` will always render with a tooltip unless the tag is `:summary`. + # `IconButton` will always render with a tooltip unless the tag is `:summary`. + # @accessibility + # `IconButton` requires an `aria-label`, which will provide assistive technologies with an accessible label. + # The `aria-label` should describe the action to be invoked rather than the icon itself. For instance, + # if your `IconButton` renders a magnifying glass icon and invokes a search action, the `aria-label` should be + # `"Search"` instead of `"Magnifying glass"`. + # Either `aria-label` or `aria-description` will be used for the `Tooltip` text, depending on which one is present. + # Either `aria-label` or `aria-description` will be used for the `Tooltip` text, depending on which one is present. + # [Learn more about best functional image practices (WAI Images)](https://www.w3.org/WAI/tutorials/images/functional) + class IconButton < Primer::Component + status :beta + + DEFAULT_SCHEME = :default + SCHEME_MAPPINGS = { + DEFAULT_SCHEME => "Button--secondary", + :danger => "Button--danger", + :invisible => "Button--invisible" + }.freeze + SCHEME_OPTIONS = SCHEME_MAPPINGS.keys + + # @example Default + # + # <%= render(Primer::Beta::IconButton.new(icon: :search, "aria-label": "Search", id: "search-button", id: "search-button")) %> + # + # @example Schemes + # + # <%= render(Primer::Beta::IconButton.new(icon: :search, "aria-label": "Search")) %> + # <%= render(Primer::Beta::IconButton.new(icon: :trash, "aria-label": "Delete", scheme: :danger)) %> + # + # @example With an `aria-description` + # @description + # If you need to have a longer description for the icon button, use both the `aria-label` and `aria-description` + # attributes. A label should be short and concise, while the description can be longer as it is intended to provide + # more context and information. See the accessibility section for more information. + # @code + # <%= render(Primer::Beta::IconButton.new(icon: :bold, "aria-label": "Bold", "aria-description": "Add bold text, Cmd+b")) %> + # + # @example Custom tooltip direction + # + # <%= render(Primer::Beta::IconButton.new(icon: :search, "aria-label": "Search", tooltip_direction: :e)) %> + # + # @param icon [String] Name of <%= link_to_octicons %> to use. + # @param scheme [Symbol] <%= one_of(Primer::Beta::IconButton::SCHEME_OPTIONS) %> + # @param size [Symbol] <%= one_of(Primer::Beta::Button::SIZE_OPTIONS) %> + # @param tag [Symbol] <%= one_of(Primer::BaseButton::TAG_OPTIONS) %> + # @param type [Symbol] <%= one_of(Primer::BaseButton::TYPE_OPTIONS) %> + # @param aria-label [String] String that can be read by assistive technology. A label should be short and concise. See the accessibility section for more information. + # @param aria-description [String] String that can be read by assistive technology. A description can be longer as it is intended to provide more context and information. See the accessibility section for more information. + # @param tooltip_direction [Symbol] (Primer::Alpha::Tooltip::DIRECTION_DEFAULT) <%= one_of(Primer::Alpha::Tooltip::DIRECTION_OPTIONS) %> + # @param system_arguments [Hash] <%= link_to_system_arguments_docs %> + def initialize(icon:, scheme: DEFAULT_SCHEME, tooltip_direction: Primer::Alpha::Tooltip::DIRECTION_DEFAULT, size: Primer::Beta::Button::DEFAULT_SIZE, **system_arguments) + @icon = icon + + @system_arguments = system_arguments + @system_arguments[:id] ||= "icon-button-#{SecureRandom.hex(4)}" + + @system_arguments[:classes] = class_names( + "Button", + "Button--iconOnly", + SCHEME_MAPPINGS[fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME)], + Primer::Beta::Button::SIZE_MAPPINGS[fetch_or_fallback(Primer::Beta::Button::SIZE_OPTIONS, size, Primer::Beta::Button::DEFAULT_SIZE)], + system_arguments[:classes] + ) + + validate_aria_label + + @aria_label = aria("label", @system_arguments) + @aria_description = aria("description", @system_arguments) + + @tooltip_arguments = { + for_id: @system_arguments[:id], + direction: tooltip_direction + } + + # If we have both an `aria-label` and a `aria-description`, we create a `Tooltip` with the description type and keep the `aria-label` in the button. + # Otherwise, the `aria-label` is used as the tooltip text, which is the `aria-labelled-by` of the button, so we don't set it in the button. + if @aria_label.present? && @aria_description.present? + @system_arguments.delete(:"aria-description") + @system_arguments[:aria].delete(:description) if @system_arguments.include?(:aria) + @tooltip_arguments[:text] = @aria_description + @tooltip_arguments[:type] = :description + else + @system_arguments.delete(:"aria-label") + @system_arguments[:aria].delete(:label) if @system_arguments.include?(:aria) + @tooltip_arguments[:text] = @aria_label + @tooltip_arguments[:type] = :label + end + end + + private + + def render_tooltip? + @system_arguments[:tag] != :summary + end + end + end +end diff --git a/app/components/primer/primer.pcss b/app/components/primer/primer.pcss index 552c1c7300..a73203e1c2 100644 --- a/app/components/primer/primer.pcss +++ b/app/components/primer/primer.pcss @@ -1 +1,2 @@ /* CSS component styles here */ +@import "./beta/button.pcss"; diff --git a/lib/tasks/docs.rake b/lib/tasks/docs.rake index 80fcb24b91..39d2492cde 100644 --- a/lib/tasks/docs.rake +++ b/lib/tasks/docs.rake @@ -29,6 +29,8 @@ namespace :docs do # Rails controller for rendering arbitrary ERB view_context = ApplicationController.new.tap { |c| c.request = ActionDispatch::TestRequest.create }.view_context components = [ + Primer::Beta::IconButton, + Primer::Beta::Button, Primer::Alpha::Layout, Primer::HellipButton, Primer::Image, @@ -107,7 +109,6 @@ namespace :docs do components_needing_docs = all_components - components args_for_components = [] - classes_found_in_examples = [] errors = [] @@ -252,9 +253,6 @@ namespace :docs do end f.puts html = view_context.render(inline: code) - html.scan(/class="([^"]*)"/) do |classnames| - classes_found_in_examples.concat(classnames[0].split.reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}" }) - end f.puts("") f.puts f.puts("```erb") @@ -276,10 +274,6 @@ namespace :docs do raise end - File.open("static/classes.yml", "w") do |f| - f.puts YAML.dump(classes_found_in_examples.sort.uniq) - end - File.open("static/arguments.yml", "w") do |f| f.puts YAML.dump(args_for_components) end diff --git a/static/arguments.yml b/static/arguments.yml index a7bb487a1f..5060e615c6 100644 --- a/static/arguments.yml +++ b/static/arguments.yml @@ -477,6 +477,38 @@ type: Hash default: N/A description: "[System arguments](/system-arguments)" +- component: Button + source: https://github.com/primer/view_components/tree/main/app/components/primer/beta/button.rb + parameters: + - name: scheme + type: Symbol + default: "`:default`" + description: One of `:danger`, `:default`, `:invisible`, `:link`, `:outline`, + `:primary`, or `:secondary`. + - name: size + type: Symbol + default: "`:medium`" + description: One of `:large`, `:medium`, or `:small`. + - name: full_width + type: Boolean + default: "`false`" + description: 'Whether button is full-width with `display: block`.' + - name: align_content + type: Symbol + default: "`:center`" + description: One of `:center` or `:start`. + - name: tag + type: Symbol + default: "`:button`" + description: One of `:a`, `:button`, or `:summary`. + - name: type + type: Symbol + default: "`:button`" + description: One of `:button`, `:reset`, or `:submit`. + - name: system_arguments + type: Hash + default: N/A + description: "[System arguments](/system-arguments)" - component: ButtonGroup source: https://github.com/primer/view_components/tree/main/app/components/primer/beta/button_group.rb parameters: @@ -591,6 +623,48 @@ type: Hash default: N/A description: "[System arguments](/system-arguments)" +- component: IconButton + source: https://github.com/primer/view_components/tree/main/app/components/primer/beta/icon_button.rb + parameters: + - name: icon + type: String + default: N/A + description: Name of [Octicon](https://primer.style/octicons/) to use. + - name: scheme + type: Symbol + default: "`:default`" + description: One of `:danger`, `:default`, or `:invisible`. + - name: size + type: Symbol + default: "`:medium`" + description: One of `:large`, `:medium`, or `:small`. + - name: tag + type: Symbol + default: N/A + description: One of `:a`, `:button`, or `:summary`. + - name: type + type: Symbol + default: N/A + description: One of `:button`, `:reset`, or `:submit`. + - name: aria-label + type: String + default: N/A + description: String that can be read by assistive technology. A label should be + short and concise. See the accessibility section for more information. + - name: aria-description + type: String + default: N/A + description: String that can be read by assistive technology. A description can + be longer as it is intended to provide more context and information. See the + accessibility section for more information. + - name: tooltip_direction + type: Symbol + default: "`:s`" + description: One of `:e`, `:n`, `:ne`, `:nw`, `:s`, `:se`, `:sw`, or `:w`. + - name: system_arguments + type: Hash + default: N/A + description: "[System arguments](/system-arguments)" - component: Text source: https://github.com/primer/view_components/tree/main/app/components/primer/beta/text.rb parameters: diff --git a/static/audited_at.json b/static/audited_at.json index 993cae9d16..5bd194aba0 100644 --- a/static/audited_at.json +++ b/static/audited_at.json @@ -24,12 +24,14 @@ "Primer::Beta::BorderBox::Header": "", "Primer::Beta::Breadcrumbs": "", "Primer::Beta::Breadcrumbs::Item": "", + "Primer::Beta::Button": "", "Primer::Beta::ButtonGroup": "", "Primer::Beta::CloseButton": "", "Primer::Beta::Counter": "", "Primer::Beta::Details": "", "Primer::Beta::Flash": "", "Primer::Beta::Heading": "", + "Primer::Beta::IconButton": "", "Primer::Beta::Text": "", "Primer::Beta::Truncate": "", "Primer::Beta::Truncate::TruncateText": "", diff --git a/static/classes.yml b/static/classes.yml deleted file mode 100644 index 0d0596eb96..0000000000 --- a/static/classes.yml +++ /dev/null @@ -1,230 +0,0 @@ ---- -- ".ActionList" -- ".ActionList-content" -- ".ActionList-item" -- ".ActionList-item-label" -- ".AvatarStack" -- ".AvatarStack--right" -- ".AvatarStack--three-plus" -- ".AvatarStack-body" -- ".Box" -- ".Box--condensed" -- ".Box-body" -- ".Box-btn-octicon" -- ".Box-footer" -- ".Box-header" -- ".Box-row" -- ".Box-row--blue" -- ".Box-row--gray" -- ".Box-row--yellow" -- ".Box-title" -- ".BtnGroup" -- ".BtnGroup-item" -- ".Counter" -- ".Counter--primary" -- ".Counter--secondary" -- ".FormControl" -- ".FormControl--fullWidth" -- ".FormControl-caption" -- ".FormControl-inlineValidation" -- ".FormControl-input" -- ".FormControl-input-leadingVisual" -- ".FormControl-input-leadingVisualWrap" -- ".FormControl-input-trailingAction" -- ".FormControl-input-wrap" -- ".FormControl-input-wrap--leadingVisual" -- ".FormControl-input-wrap--trailingAction" -- ".FormControl-inset" -- ".FormControl-label" -- ".FormControl-medium" -- ".FormControl-monospace" -- ".Label" -- ".Label--accent" -- ".Label--attention" -- ".Label--danger" -- ".Label--done" -- ".Label--inline" -- ".Label--large" -- ".Label--primary" -- ".Label--secondary" -- ".Label--severe" -- ".Label--sponsors" -- ".Label--success" -- ".Layout" -- ".Layout--flowRow-until-lg" -- ".Layout--flowRow-until-md" -- ".Layout--sidebar-narrow" -- ".Layout--sidebar-wide" -- ".Layout--sidebarPosition-end" -- ".Layout--sidebarPosition-flowRow-end" -- ".Layout--sidebarPosition-flowRow-none" -- ".Layout--sidebarPosition-flowRow-start" -- ".Layout--sidebarPosition-start" -- ".Layout-main" -- ".Layout-main-centered-lg" -- ".Layout-main-centered-md" -- ".Layout-main-centered-xl" -- ".Layout-sidebar" -- ".Link--muted" -- ".Link--primary" -- ".Link--secondary" -- ".Overlay" -- ".Overlay--height-auto" -- ".Overlay--width-auto" -- ".Overlay-backdrop--anchor" -- ".Overlay-body" -- ".Overlay-body--paddingNone" -- ".Popover" -- ".Popover-message" -- ".Popover-message--large" -- ".Popover-message--left" -- ".Progress" -- ".Progress--large" -- ".Progress--small" -- ".Progress-item" -- ".State" -- ".State--closed" -- ".State--merged" -- ".State--open" -- ".State--small" -- ".Subhead" -- ".Subhead-actions" -- ".Subhead-description" -- ".Subhead-heading" -- ".Subhead-heading--danger" -- ".TimelineItem" -- ".TimelineItem-avatar" -- ".TimelineItem-badge" -- ".TimelineItem-body" -- ".Truncate" -- ".Truncate-text" -- ".Truncate-text--expandable" -- ".Truncate-text--primary" -- ".UnderlineNav" -- ".UnderlineNav--right" -- ".UnderlineNav-actions" -- ".UnderlineNav-body" -- ".UnderlineNav-item" -- ".UnderlineNav-octicon" -- ".anim-rotate" -- ".avatar" -- ".avatar-more" -- ".avatar-small" -- ".blankslate" -- ".blankslate-action" -- ".blankslate-heading" -- ".blankslate-icon" -- ".blankslate-image" -- ".blankslate-narrow" -- ".blankslate-spacious" -- ".border" -- ".border-bottom-0" -- ".breadcrumb-item" -- ".breadcrumb-item-selected" -- ".btn" -- ".btn-block" -- ".btn-danger" -- ".btn-invisible" -- ".btn-large-mktg" -- ".btn-link" -- ".btn-mktg" -- ".btn-muted-mktg" -- ".btn-octicon" -- ".btn-octicon-danger" -- ".btn-outline" -- ".btn-primary" -- ".btn-signup-mktg" -- ".btn-sm" -- ".btn-subtle-mktg" -- ".circle" -- ".close-button" -- ".col-2" -- ".col-3" -- ".col-9" -- ".color-bg-accent-emphasis" -- ".color-bg-danger-emphasis" -- ".color-bg-emphasis" -- ".color-bg-subtle" -- ".color-bg-success-emphasis" -- ".color-border-accent-emphasis" -- ".color-fg-danger" -- ".color-fg-on-emphasis" -- ".color-fg-success" -- ".color-shadow-large" -- ".container-lg" -- ".container-md" -- ".container-xl" -- ".css-truncate" -- ".css-truncate-overflow" -- ".css-truncate-target" -- ".custom-class" -- ".d-flex" -- ".d-inline-block" -- ".d-inline-flex" -- ".details-overlay" -- ".details-reset" -- ".dropdown" -- ".dropdown-divider" -- ".dropdown-header" -- ".dropdown-item" -- ".dropdown-menu" -- ".dropdown-menu-s" -- ".dropdown-menu-se" -- ".ellipsis-expander" -- ".expandable" -- ".flash" -- ".flash-action" -- ".flash-close" -- ".flash-error" -- ".flash-full" -- ".flash-success" -- ".flash-warn" -- ".flex-1" -- ".flex-auto" -- ".flex-column" -- ".flex-items-end" -- ".flex-justify-center" -- ".flex-shrink-0" -- ".float-right" -- ".gutter-condensed" -- ".gutter-lg" -- ".hidden-text-expander" -- ".inline" -- ".left-0" -- ".lh-0" -- ".list-style-none" -- ".m-2" -- ".markdown-body" -- ".mb-0" -- ".mb-2" -- ".menu" -- ".menu-heading" -- ".menu-item" -- ".ml-2" -- ".mr-2" -- ".mr-n1" -- ".mt-2" -- ".mt-5" -- ".mx-auto" -- ".no-underline" -- ".no-wrap" -- ".p-1" -- ".p-3" -- ".p-4" -- ".p-5" -- ".position-absolute" -- ".position-relative" -- ".pr-2" -- ".pt-5" -- ".right-0" -- ".sr-only" -- ".tabnav" -- ".tabnav-tab" -- ".tabnav-tabs" -- ".text-bold" -- ".text-left" -- ".text-normal" -- ".tooltipped" -- ".tooltipped-n" -- ".tooltipped-no-delay" -- ".tooltipped-s" diff --git a/static/constants.json b/static/constants.json index 6a3aacf787..25ca14389a 100644 --- a/static/constants.json +++ b/static/constants.json @@ -320,6 +320,48 @@ }, "Primer::Beta::Breadcrumbs::Item": { }, + "Primer::Beta::Button": { + "ALIGN_CONTENT_MAPPINGS": { + "start": "Button-content--alignStart", + "center": "" + }, + "ALIGN_CONTENT_OPTIONS": [ + "start", + "center" + ], + "DEFAULT_ALIGN_CONTENT": "center", + "DEFAULT_SCHEME": "default", + "DEFAULT_SIZE": "medium", + "LINK_SCHEME": "link", + "SCHEME_MAPPINGS": { + "default": "Button--secondary", + "primary": "Button--primary", + "secondary": "Button--secondary", + "danger": "Button--danger", + "outline": "btn-outline", + "invisible": "Button--invisible", + "link": "btn-link" + }, + "SCHEME_OPTIONS": [ + "default", + "primary", + "secondary", + "danger", + "outline", + "invisible", + "link" + ], + "SIZE_MAPPINGS": { + "small": "Button--small", + "medium": "Button--medium", + "large": "Button--large" + }, + "SIZE_OPTIONS": [ + "small", + "medium", + "large" + ] + }, "Primer::Beta::ButtonGroup": { }, "Primer::Beta::CloseButton": { @@ -383,6 +425,19 @@ "h6" ] }, + "Primer::Beta::IconButton": { + "DEFAULT_SCHEME": "default", + "SCHEME_MAPPINGS": { + "default": "Button--secondary", + "danger": "Button--danger", + "invisible": "Button--invisible" + }, + "SCHEME_OPTIONS": [ + "default", + "danger", + "invisible" + ] + }, "Primer::Beta::Text": { "DEFAULT_TAG": "span" }, diff --git a/static/statuses.json b/static/statuses.json index 794f07ec18..e4defac50c 100644 --- a/static/statuses.json +++ b/static/statuses.json @@ -24,12 +24,14 @@ "Primer::Beta::BorderBox::Header": "beta", "Primer::Beta::Breadcrumbs": "beta", "Primer::Beta::Breadcrumbs::Item": "alpha", + "Primer::Beta::Button": "beta", "Primer::Beta::ButtonGroup": "beta", "Primer::Beta::CloseButton": "beta", "Primer::Beta::Counter": "beta", "Primer::Beta::Details": "beta", "Primer::Beta::Flash": "beta", "Primer::Beta::Heading": "beta", + "Primer::Beta::IconButton": "beta", "Primer::Beta::Text": "beta", "Primer::Beta::Truncate": "beta", "Primer::Beta::Truncate::TruncateText": "alpha", diff --git a/test/components/component_test.rb b/test/components/component_test.rb index 7311e36d74..b528da9602 100644 --- a/test/components/component_test.rb +++ b/test/components/component_test.rb @@ -7,6 +7,8 @@ class PrimerComponentTest < Minitest::Test # Components with any arguments necessary to make them render COMPONENTS_WITH_ARGS = [ + [Primer::Beta::IconButton, { icon: :star, "aria-label": "Star" }], + [Primer::Beta::Button, {}], [Primer::Alpha::Layout, {}, proc { |component| component.main(tag: :div) { "Foo" } component.sidebar(tag: :div) { "Bar" } diff --git a/test/components/primer/beta/button_test.rb b/test/components/primer/beta/button_test.rb new file mode 100644 index 0000000000..58269ed85b --- /dev/null +++ b/test/components/primer/beta/button_test.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "test_helper" + +class PrimerBetaButtonTest < Minitest::Test + include Primer::ComponentTestHelpers + + def test_renders + render_inline(Primer::Beta::Button.new) { "Button" } + + assert_selector(".Button", text: "Button") + end +end diff --git a/test/components/primer/beta/icon_button_test.rb b/test/components/primer/beta/icon_button_test.rb new file mode 100644 index 0000000000..b52007eb45 --- /dev/null +++ b/test/components/primer/beta/icon_button_test.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "test_helper" + +class PrimerBetaIconButtonTest < Minitest::Test + include Primer::ComponentTestHelpers + + def test_renders + render_inline(Primer::Beta::IconButton.new(icon: :star, "aria-label": "Star")) + + assert_selector(".Button.Button--iconOnly") do + assert_selector(".Button-visual") + end + assert_selector("tool-tip", text: "Star", visible: :all) + end +end diff --git a/test/lib/css_coverage_test.rb b/test/lib/css_coverage_test.rb index 870d5fd772..20753507d0 100644 --- a/test/lib/css_coverage_test.rb +++ b/test/lib/css_coverage_test.rb @@ -10,15 +10,6 @@ def setup .map { |k| ".#{k}" } .uniq - @classes_from_docs_build = - YAML.safe_load( - File.read( - File.join( - __FILE__.split("test")[0], "/static/classes.yml" - ) - ) - ) - @allowed_missing_classes_for_now = [ # used to showcase custom classes in component docs ".custom-class", @@ -45,8 +36,4 @@ def setup def test_classify_does_not_generate_primer_css_classes_that_do_not_exist assert_empty(@classes_from_utilities - @css_data - @allowed_missing_classes_for_now) end - - def test_docs_do_not_generate_primer_css_classes_that_do_not_exist - assert_empty(@classes_from_docs_build - @css_data - @allowed_missing_classes_for_now) - end end diff --git a/test/previews/primer/beta/button_preview.rb b/test/previews/primer/beta/button_preview.rb new file mode 100644 index 0000000000..f5f6f87d48 --- /dev/null +++ b/test/previews/primer/beta/button_preview.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Primer + module Beta + # @label Button + class ButtonPreview < ViewComponent::Preview + # @label Playground + # @param scheme select [default, primary, danger, outline, invisible, link] + # @param size select [small, medium, large] + # @param full_width toggle + # @param disabled toggle + # @param pressed toggle + # @param align_content select [center, start] + # @param tag select [a, summary, button] + def playground( + scheme: :default, + size: :medium, + full_width: false, + id: "button-preview", + align_content: :center, + tag: :button, + disabled: false, + pressed: false + ) + render(Primer::Beta::Button.new( + scheme: scheme, + size: size, + full_width: full_width, + id: id, + align_content: align_content, + tag: tag, + disabled: disabled, + "aria-pressed": pressed + )) do |_c| + "Button" + end + end + + # @label With visuals + # @param scheme select [default, primary, danger, outline, invisible, link] + # @param size select [small, medium] + # @param full_width toggle + # @param align_content select [center, start] + def with_visuals( + scheme: :default, + size: :medium, + full_width: false, + id: "button-preview", + align_content: :center + ) + render_with_template(locals: { + scheme: scheme, + size: size, + full_width: full_width, + id: id, + align_content: align_content + }) + end + + # @label Link as button + # @param scheme select [default, primary, danger, outline, invisible, link] + # @param size select [small, medium] + # @param full_width toggle + # @param align_content select [center, start] + + def link_as_button( + scheme: :default, + size: :medium, + full_width: false, + id: "button-preview", + align_content: :center, + tag: :a + ) + render(Primer::Beta::Button.new( + scheme: scheme, + size: size, + full_width: full_width, + id: id, + align_content: align_content, + tag: tag + )) do |_c| + "Button" + end + end + end + end +end diff --git a/test/previews/primer/beta/button_preview/with_visuals.html.erb b/test/previews/primer/beta/button_preview/with_visuals.html.erb new file mode 100644 index 0000000000..030324d134 --- /dev/null +++ b/test/previews/primer/beta/button_preview/with_visuals.html.erb @@ -0,0 +1,12 @@ +<%= render(Primer::Beta::Button.new( + scheme: scheme, + size: size, + full_width: full_width, + id: id, + align_content: align_content +)) do |c| %> + <% c.leading_visual_icon(icon: :star) %> + <% c.trailing_visual_icon(icon: :star) %> + <% c.with_tooltip(text: "Tooltip text") %> + Button +<% end %> diff --git a/test/previews/primer/beta/icon_button_preview.rb b/test/previews/primer/beta/icon_button_preview.rb new file mode 100644 index 0000000000..3354091bc9 --- /dev/null +++ b/test/previews/primer/beta/icon_button_preview.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Primer + module Beta + # @label IconButton + class IconButtonPreview < ViewComponent::Preview + # @label Playground + # @param scheme select [default, danger, invisible] + # @param size select [small, medium, large] + # @param aria_label text + # @param disabled toggle + # @param pressed toggle + # @param tag select [a, summary, button] + def playground( + scheme: :default, + size: :medium, + id: "button-preview", + tag: :button, + disabled: false, + icon: :star, + aria_label: "Button" + ) + render(Primer::Beta::IconButton.new( + scheme: scheme, + size: size, + id: id, + tag: tag, + disabled: disabled, + icon: icon, + "aria-label": aria_label + )) do |_c| + "Button" + end + end + end + end +end