diff --git a/.changeset/nice-days-jog.md b/.changeset/nice-days-jog.md new file mode 100644 index 0000000000..fc39db461f --- /dev/null +++ b/.changeset/nice-days-jog.md @@ -0,0 +1,7 @@ +--- +"@primer/css": major +--- + +UnderlineNav `:focus` styles +Refactor selected state and spacing +Add selected bold state override from github/github diff --git a/.changeset/orange-ties-sin.md b/.changeset/orange-ties-sin.md new file mode 100644 index 0000000000..fbb368a808 --- /dev/null +++ b/.changeset/orange-ties-sin.md @@ -0,0 +1,5 @@ +--- +"@primer/css": patch +--- + +Use `counter-border` for LHC diff --git a/docs/src/stories/components/Navigation/UnderlineNav.stories.jsx b/docs/src/stories/components/Navigation/UnderlineNav.stories.jsx new file mode 100644 index 0000000000..a1e5a76dcb --- /dev/null +++ b/docs/src/stories/components/Navigation/UnderlineNav.stories.jsx @@ -0,0 +1,73 @@ +import React from 'react' +import clsx from 'clsx' +import {UnderlineNavItemTemplate} from './UnderlineNavItem.stories' + +export default { + title: 'Components/Navigation/UnderlineNav', + excludeStories: ['UnderlineNavTemplate'], + layout: 'padded', + argTypes: { + variant: { + options: [0, 1, 2], // iterator + mapping: ['', 'UnderlineNav--right', 'UnderlineNav--full'], // values + control: { + type: 'inline-radio', + labels: ['default', 'align-right', 'fullwidth'] + }, + description: 'nav positioning options', + table: { + category: 'CSS' + } + }, + children: { + description: 'creates a slot for children', + table: { + category: 'HTML' + } + }, + actionStart: { + description: 'action to left of nav', + table: { + category: 'HTML' + } + }, + actionEnd: { + description: 'action to right of nav', + table: { + category: 'HTML' + } + } + } +} + +export const UnderlineNavTemplate = ({variant, children, actionStart, actionEnd}) => ( + <> + + +) + +export const Playground = UnderlineNavTemplate.bind({}) +Playground.args = { + variant: 0, + children: ( + <> + + + + + ) +} diff --git a/docs/src/stories/components/Navigation/UnderlineNavAction.stories.jsx b/docs/src/stories/components/Navigation/UnderlineNavAction.stories.jsx new file mode 100644 index 0000000000..7e43dd03f8 --- /dev/null +++ b/docs/src/stories/components/Navigation/UnderlineNavAction.stories.jsx @@ -0,0 +1,56 @@ +import React from 'react' +import clsx from 'clsx' +import {ButtonTemplate} from '../Button/Button.stories' +import {LinkTemplate} from '../Link/Link.stories' + +export default { + title: 'Components/Navigation/UnderlineNav/Action', + excludeStories: ['UnderlineNavActionTemplate'], + layout: 'padded', + argTypes: { + semanticItemType: { + options: ['button', 'link'], + control: { + type: 'inline-radio' + }, + description: 'item can be a button or a link', + table: { + category: 'HTML' + } + }, + label: { + name: 'label', + type: 'string', + description: 'Item label text', + table: { + category: 'HTML' + } + }, + focusElement: { + control: {type: 'boolean'}, + description: 'set manual focus on item', + table: { + category: 'Interactive' + } + } + } +} + +export const UnderlineNavActionTemplate = ({semanticItemType, label, focusElement}) => { + return ( +
+ {semanticItemType === 'button' ? ( + + ) : ( + + )} +
+ ) +} + +export const Playground = UnderlineNavActionTemplate.bind({}) +Playground.args = { + semanticItemType: 'button', + label: 'Action', + focusElement: false +} diff --git a/docs/src/stories/components/Navigation/UnderlineNavItem.stories.jsx b/docs/src/stories/components/Navigation/UnderlineNavItem.stories.jsx new file mode 100644 index 0000000000..da27a1227f --- /dev/null +++ b/docs/src/stories/components/Navigation/UnderlineNavItem.stories.jsx @@ -0,0 +1,140 @@ +import React from 'react' +import clsx from 'clsx' +import useToggle from '../../helpers/useToggle.jsx' + +export default { + title: 'Components/Navigation/UnderlineNav/Item', + excludeStories: ['UnderlineNavItemTemplate'], + layout: 'padded', + argTypes: { + selected: { + control: {type: 'boolean'}, + description: 'active nav item', + table: { + category: 'CSS' + } + }, + usesDataContent: { + control: {type: 'boolean'}, + description: 'creates a hidden label to allow for bold text without layout shift', + table: { + category: 'CSS' + } + }, + semanticItemType: { + options: ['button', 'link'], + control: { + type: 'inline-radio' + }, + description: 'item can be a button or a link', + table: { + category: 'HTML' + } + }, + label: { + name: 'label', + type: 'string', + description: 'Item label text', + table: { + category: 'HTML' + } + }, + focusElement: { + control: {type: 'boolean'}, + description: 'set manual focus on tab item', + table: { + category: 'Interactive' + } + }, + icon: { + control: {type: 'boolean'}, + description: 'show icon', + table: { + category: 'CSS' + } + }, + counter: { + control: {type: 'boolean'}, + description: 'show counter', + table: { + category: 'CSS' + } + } + } +} + +export const UnderlineNavItemTemplate = ({ + semanticItemType, + label, + selected, + focusElement, + icon, + counter, + usesDataContent +}) => { + const [isSelected, itemisSelected] = useToggle() + return ( +
  • + {semanticItemType === 'button' ? ( + + ) : ( + + {icon && ( + + + + )} + {label} + {counter && 10} + + )} +
  • + ) +} + +export const Playground = UnderlineNavItemTemplate.bind({}) +Playground.args = { + semanticItemType: 'button', + label: 'Item', + selected: false, + focusElement: false, + icon: false, + counter: false, + usesDataContent: true +} diff --git a/docs/src/stories/components/Navigation/UnderlineNavPatterns.stories.jsx b/docs/src/stories/components/Navigation/UnderlineNavPatterns.stories.jsx new file mode 100644 index 0000000000..f5b152b6b0 --- /dev/null +++ b/docs/src/stories/components/Navigation/UnderlineNavPatterns.stories.jsx @@ -0,0 +1,111 @@ +import React from 'react' +import clsx from 'clsx' +import {UnderlineNavTemplate} from './UnderlineNav.stories' +import {UnderlineNavItemTemplate} from './UnderlineNavItem.stories' +import {UnderlineNavActionTemplate} from './UnderlineNavAction.stories' + +export default { + title: 'Components/Navigation/UnderlineNav/Features', + layout: 'padded' +} + +export const LinkItems = UnderlineNavTemplate.bind({}) +LinkItems.args = { + children: ( + <> + + + + + ) +} + +export const ButtonItems = UnderlineNavTemplate.bind({}) +ButtonItems.args = { + children: ( + <> + + + + + ) +} + +export const NavRight = UnderlineNavTemplate.bind({}) +NavRight.args = { + variant: 'UnderlineNav--right', + children: ( + <> + + + + + ) +} + +export const NavFullWidth = UnderlineNavTemplate.bind({}) +NavFullWidth.args = { + variant: 'UnderlineNav--full', + children: ( + <> + + + + + ) +} + +export const ActionRight = UnderlineNavTemplate.bind({}) +ActionRight.args = { + children: ( + <> + + + + + ), + actionEnd: +} + +export const ActionLeft = UnderlineNavTemplate.bind({}) +ActionLeft.args = { + children: ( + <> + + + + + ), + actionStart: +} + +export const Overflow = UnderlineNavTemplate.bind({}) +Overflow.args = { + children: ( + <> + + + + + + + + + + + + ) +} + +export const Icons = UnderlineNavTemplate.bind({}) +Icons.args = { + children: ( + <> + + + + + + + ) +} diff --git a/src/labels/counters.scss b/src/labels/counters.scss index ed71943623..d170b91a53 100644 --- a/src/labels/counters.scss +++ b/src/labels/counters.scss @@ -11,7 +11,7 @@ color: var(--color-fg-default); text-align: center; background-color: var(--color-neutral-muted); - border: $border-width $border-style transparent; // Support Firefox custom colors + border: $border-width $border-style var(--color-counter-border); // stylelint-disable-next-line primer/borders border-radius: 2em; diff --git a/src/navigation/underline-nav.scss b/src/navigation/underline-nav.scss index bac88d8aa4..c6cb37cd18 100644 --- a/src/navigation/underline-nav.scss +++ b/src/navigation/underline-nav.scss @@ -1,50 +1,99 @@ +$nav-height: $spacer-3 * 3 !default; // 48px + .UnderlineNav { display: flex; + min-height: $nav-height; overflow-x: auto; overflow-y: hidden; // stylelint-disable-next-line primer/box-shadow box-shadow: inset 0 -1px 0 var(--color-border-muted); + -webkit-overflow-scrolling: auto; justify-content: space-between; } .UnderlineNav-body { display: flex; + align-items: center; + gap: $spacer-2; + list-style: none; } .UnderlineNav-item { - padding: $spacer-2 $spacer-3; + position: relative; + display: flex; + padding: 0 $spacer-2; font-size: $body-font-size; // stylelint-disable-next-line primer/typography line-height: 30px; color: var(--color-fg-default); text-align: center; white-space: nowrap; + cursor: pointer; background-color: transparent; border: 0; - // stylelint-disable-next-line primer/borders - border-bottom: 2px $border-style transparent; + border-radius: $border-radius; + align-items: center; - &:hover, - &:focus { - color: var(--color-fg-default); - text-decoration: none; - border-bottom-color: var(--color-neutral-muted); - outline: 1px dotted transparent; // Support Firefox custom colors - outline-offset: -1px; - transition: border-bottom-color 0.12s ease-out; + // renders a visibly hidden "copy" of the label in bold, reserving box space for when label becomes bold on selected + [data-content]::before { + display: block; + height: 0; + font-weight: $font-weight-bold; + visibility: hidden; + content: attr(data-content); + } + + // increase touch target area + &::before { + @include minTouchTarget($min-height: $nav-height); + } + + // hover state was "sticking" on mobile after click + @media (pointer: fine) { + &:hover { + color: var(--color-fg-default); + text-decoration: none; + background: var(--color-action-list-item-default-hover-bg); + transition: background 0.12s ease-out; + } } &.selected, - &[role=tab][aria-selected=true], - &[aria-current]:not([aria-current=false]) { + &[role='tab'][aria-selected='true'], + &[aria-current]:not([aria-current='false']) { font-weight: $font-weight-bold; color: var(--color-fg-default); - border-bottom-color: var(--color-primer-border-active); - outline: 1px dotted transparent; // Support Firefox custom colors - outline-offset: -1px; - .UnderlineNav-octicon { - color: var(--color-fg-muted); + // current/selected underline + &::after { + position: absolute; + right: 50%; + // 48px total height / 2 (24px) + 1px + bottom: calc(50% - 25px); + width: 100%; + height: 2px; + content: ''; + background: var(--color-primer-border-active); + border-radius: $border-radius; + transform: translate(50%, -50%); + } + } + + // remove when global focus state is merged + &.focus, + &:focus { + @include focusOutline; + outline-offset: -2px; + } + + .Counter { + margin-left: $spacer-2; + color: var(--color-fg-default); + background-color: var(--color-neutral-muted); + + &--primary { + color: var(--color-fg-on-emphasis); + background-color: var(--color-neutral-emphasis); } } } @@ -63,22 +112,18 @@ .UnderlineNav--full { display: block; -} -.UnderlineNav-octicon { - margin-right: $spacer-1; - color: var(--color-fg-subtle); + // required for underline to align with additional wrapper element + .UnderlineNav-body { + min-height: $nav-height; + } } -.UnderlineNav .Counter { - margin-left: $spacer-1; - color: var(--color-fg-default); - background-color: var(--color-neutral-muted); - - &--primary { - color: var(--color-fg-on-emphasis); - background-color: var(--color-neutral-emphasis); - } +.UnderlineNav-octicon { + display: inline !important; + margin-right: $spacer-2; + color: var(--color-fg-muted); + fill: var(--color-fg-muted); } .UnderlineNav-container { diff --git a/src/support/mixins/misc.scss b/src/support/mixins/misc.scss index c27ac96a75..8c7f4c77b8 100644 --- a/src/support/mixins/misc.scss +++ b/src/support/mixins/misc.scss @@ -24,3 +24,27 @@ background-color: $border; } } + +// global focus styles + +@mixin focusOutline { + z-index: 1; + outline: 2px solid var(--color-accent-fg); + outline-offset: 2px; +} + +// if min-width is undefined, return only min-height +@mixin minTouchTarget($min-height: 32px, $min-width: '') { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + min-height: $min-height; + content: ''; + transform: translateX(-50%) translateY(-50%); + + @if $min-width != '' { + min-width: $min-width; + } +}