1
1
mirror of https://github.com/primer/css.git synced 2024-09-17 19:57:26 +03:00

UnderlineNav :focus styles (#1764)

* add pseudo selectors

* adjustments

* add stories, cleanup

* update mixin

* fix mixin

* lint

* add back overflow styles

* adjust focus for better overflow state, scrollsnap

* post test adjustments, move hacks to primer css

* Stylelint auto-fixes

* hover state desktop only

* document data-content hack

* Create nice-days-jog.md

Co-authored-by: Actions Auto Build <actions@github.com>
Co-authored-by: simurai <simurai@github.com>
This commit is contained in:
Katie Langerman 2021-12-07 12:13:49 -08:00 committed by GitHub
parent b1c43f1f8d
commit cdd9728c7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 487 additions and 31 deletions

View File

@ -0,0 +1,7 @@
---
"@primer/css": minor
---
UnderlineNav `:focus` styles
Refactor selected state and spacing
Add selected bold state override from github/github

View File

@ -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}) => (
<>
<nav className={clsx('UnderlineNav', variant && `${variant}`)}>
{actionStart}
{variant === 'UnderlineNav--full' ? (
<div class="container-lg UnderlineNav-container">
<ul class="UnderlineNav-body" role="tablist">
{children}
</ul>
</div>
) : (
<ul class="UnderlineNav-body" role="tablist">
{children}
</ul>
)}
{actionEnd}
</nav>
</>
)
export const Playground = UnderlineNavTemplate.bind({})
Playground.args = {
variant: 0,
children: (
<>
<UnderlineNavItemTemplate label="Item" semanticItemType="button" selected usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="button" usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="button" usesDataContent />
</>
)
}

View File

@ -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 (
<div class="UnderlineNav-actions">
{semanticItemType === 'button' ? (
<ButtonTemplate label={label} focusAllElements={focusElement} />
) : (
<LinkTemplate label={label} focusAllElements={focusElement} />
)}
</div>
)
}
export const Playground = UnderlineNavActionTemplate.bind({})
Playground.args = {
semanticItemType: 'button',
label: 'Action',
focusElement: false
}

View File

@ -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 (
<li className="d-inline-flex">
{semanticItemType === 'button' ? (
<button
className={clsx('UnderlineNav-item', focusElement && 'focus')}
role="tab"
aria-selected={selected || isSelected ? 'true' : undefined}
onClick={itemisSelected}
>
{icon && (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width="16"
height="16"
className="UnderlineNav-octicon"
>
<path
fill-rule="evenodd"
d="M14.064 0a8.75 8.75 0 00-6.187 2.563l-.459.458c-.314.314-.616.641-.904.979H3.31a1.75 1.75 0 00-1.49.833L.11 7.607a.75.75 0 00.418 1.11l3.102.954c.037.051.079.1.124.145l2.429 2.428c.046.046.094.088.145.125l.954 3.102a.75.75 0 001.11.418l2.774-1.707a1.75 1.75 0 00.833-1.49V9.485c.338-.288.665-.59.979-.904l.458-.459A8.75 8.75 0 0016 1.936V1.75A1.75 1.75 0 0014.25 0h-.186zM10.5 10.625c-.088.06-.177.118-.266.175l-2.35 1.521.548 1.783 1.949-1.2a.25.25 0 00.119-.213v-2.066zM3.678 8.116L5.2 5.766c.058-.09.117-.178.176-.266H3.309a.25.25 0 00-.213.119l-1.2 1.95 1.782.547zm5.26-4.493A7.25 7.25 0 0114.063 1.5h.186a.25.25 0 01.25.25v.186a7.25 7.25 0 01-2.123 5.127l-.459.458a15.21 15.21 0 01-2.499 2.02l-2.317 1.5-2.143-2.143 1.5-2.317a15.25 15.25 0 012.02-2.5l.458-.458h.002zM12 5a1 1 0 11-2 0 1 1 0 012 0zm-8.44 9.56a1.5 1.5 0 10-2.12-2.12c-.734.73-1.047 2.332-1.15 3.003a.23.23 0 00.265.265c.671-.103 2.273-.416 3.005-1.148z"
></path>
</svg>
)}
<span data-content={usesDataContent ? label : undefined}>{label}</span>
{counter && <span class="Counter">10</span>}
</button>
) : (
<a
className={clsx('UnderlineNav-item', focusElement && 'focus')}
aria-current={selected || isSelected ? 'true' : undefined}
onClick={itemisSelected}
// aria-current={selected ? 'page' : undefined}
>
{icon && (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width="16"
height="16"
className="UnderlineNav-octicon"
>
<path
fill-rule="evenodd"
d="M14.064 0a8.75 8.75 0 00-6.187 2.563l-.459.458c-.314.314-.616.641-.904.979H3.31a1.75 1.75 0 00-1.49.833L.11 7.607a.75.75 0 00.418 1.11l3.102.954c.037.051.079.1.124.145l2.429 2.428c.046.046.094.088.145.125l.954 3.102a.75.75 0 001.11.418l2.774-1.707a1.75 1.75 0 00.833-1.49V9.485c.338-.288.665-.59.979-.904l.458-.459A8.75 8.75 0 0016 1.936V1.75A1.75 1.75 0 0014.25 0h-.186zM10.5 10.625c-.088.06-.177.118-.266.175l-2.35 1.521.548 1.783 1.949-1.2a.25.25 0 00.119-.213v-2.066zM3.678 8.116L5.2 5.766c.058-.09.117-.178.176-.266H3.309a.25.25 0 00-.213.119l-1.2 1.95 1.782.547zm5.26-4.493A7.25 7.25 0 0114.063 1.5h.186a.25.25 0 01.25.25v.186a7.25 7.25 0 01-2.123 5.127l-.459.458a15.21 15.21 0 01-2.499 2.02l-2.317 1.5-2.143-2.143 1.5-2.317a15.25 15.25 0 012.02-2.5l.458-.458h.002zM12 5a1 1 0 11-2 0 1 1 0 012 0zm-8.44 9.56a1.5 1.5 0 10-2.12-2.12c-.734.73-1.047 2.332-1.15 3.003a.23.23 0 00.265.265c.671-.103 2.273-.416 3.005-1.148z"
></path>
</svg>
)}
<span data-content={usesDataContent ? label : undefined}>{label}</span>
{counter && <span class="Counter">10</span>}
</a>
)}
</li>
)
}
export const Playground = UnderlineNavItemTemplate.bind({})
Playground.args = {
semanticItemType: 'button',
label: 'Item',
selected: false,
focusElement: false,
icon: false,
counter: false,
usesDataContent: true
}

View File

@ -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: (
<>
<UnderlineNavItemTemplate label="Item" semanticItemType="link" selectedusesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
</>
)
}
export const ButtonItems = UnderlineNavTemplate.bind({})
ButtonItems.args = {
children: (
<>
<UnderlineNavItemTemplate label="Item" semanticItemType="button" selected usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="button" usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="button" usesDataContent />
</>
)
}
export const NavRight = UnderlineNavTemplate.bind({})
NavRight.args = {
variant: 'UnderlineNav--right',
children: (
<>
<UnderlineNavItemTemplate label="Item" semanticItemType="link" selected usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
</>
)
}
export const NavFullWidth = UnderlineNavTemplate.bind({})
NavFullWidth.args = {
variant: 'UnderlineNav--full',
children: (
<>
<UnderlineNavItemTemplate label="Item" semanticItemType="link" selected usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
</>
)
}
export const ActionRight = UnderlineNavTemplate.bind({})
ActionRight.args = {
children: (
<>
<UnderlineNavItemTemplate label="Item" semanticItemType="link" selected usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
</>
),
actionEnd: <UnderlineNavActionTemplate label="Action" semanticItemType="button" />
}
export const ActionLeft = UnderlineNavTemplate.bind({})
ActionLeft.args = {
children: (
<>
<UnderlineNavItemTemplate label="Item" semanticItemType="link" selected usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item" semanticItemType="link" usesDataContent />
</>
),
actionStart: <UnderlineNavActionTemplate label="Action" semanticItemType="button" />
}
export const Overflow = UnderlineNavTemplate.bind({})
Overflow.args = {
children: (
<>
<UnderlineNavItemTemplate label="Item name 1" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item name 2" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item name 3" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item name 4" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item name 5" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item name 6" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item name 7" semanticItemType="link" selected />
<UnderlineNavItemTemplate label="Item name 8" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item name 9" semanticItemType="link" usesDataContent />
<UnderlineNavItemTemplate label="Item name 10" semanticItemType="link" usesDataContent />
</>
)
}
export const Icons = UnderlineNavTemplate.bind({})
Icons.args = {
children: (
<>
<UnderlineNavItemTemplate label="Item name" semanticItemType="link" icon usesDataContent />
<UnderlineNavItemTemplate label="Item name" semanticItemType="link" icon usesDataContent />
<UnderlineNavItemTemplate label="Item name" semanticItemType="link" selected icon usesDataContent />
<UnderlineNavItemTemplate label="Item name" semanticItemType="link" icon usesDataContent />
<UnderlineNavItemTemplate label="Item name" semanticItemType="link" icon usesDataContent />
</>
)
}

View File

@ -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;
// required for underline to align with additional wrapper element
.UnderlineNav-body {
min-height: $nav-height;
}
}
.UnderlineNav-octicon {
margin-right: $spacer-1;
color: var(--color-fg-subtle);
}
.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);
}
display: inline !important;
margin-right: $spacer-2;
color: var(--color-fg-muted);
fill: var(--color-fg-muted);
}
.UnderlineNav-container {

View File

@ -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;
}
}