Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion content/actions/concepts/runners/runner-scale-sets.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ contentType: concepts

A runner scale set is a group of homogeneous runners that can be assigned jobs from {% data variables.product.prodname_actions %}. The number of active runners owned by a runner scale set can be controlled by auto-scaling runner solutions such as {% data variables.product.prodname_actions_runner_controller %} (ARC).

You can use runner groups to manage runner scale sets. Similar to self-hosted runners, you can add runner scale sets to existing runner groups. However, runner scale sets can belong to only one runner group at a time and can only have one label assigned to them.
You can use runner groups to manage runner scale sets. Similar to self-hosted runners, you can add runner scale sets to existing runner groups. However, runner scale sets can belong to only one runner group at a time. A runner scale set can have more than one label, and workflows can target it by its name or by its labels in `runs-on`. Support for multiple labels was added in Actions Runner Controller 0.14.0.

To assign jobs to a runner scale set, you must configure your workflow to reference the runner scale set’s name. For more information, see [AUTOTITLE](/actions/how-tos/manage-runners/use-actions-runner-controller/use-arc-in-a-workflow).

Expand Down
17 changes: 17 additions & 0 deletions src/fixtures/tests/playwright-rendering.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,23 @@ test.describe('test nav at different viewports', () => {
await expect(page.getByTestId('sidebar')).toBeHidden()
})

test('secondary-bar icon buttons show a label tooltip that Escape dismisses', async ({
page,
}) => {
await page.setViewportSize({ width: 1400, height: 700 })
await page.goto('/get-started/foo/bar')

const toggle = page.getByRole('button', { name: 'Collapse sidebar' })
await expect(toggle).not.toHaveAttribute('aria-label')
const tooltip = page.locator(`[id="${await toggle.getAttribute('aria-labelledby')}"]`)
await expect(tooltip).toBeHidden()
await toggle.focus()
await expect(tooltip).toBeVisible()
await expect(tooltip).toHaveText('Collapse sidebar')
await page.keyboard.press('Escape')
await expect(tooltip).toBeHidden()
})

for (const { name, width } of [
{ name: 'medium viewports - 768-1011', width: 1000 },
{ name: 'small viewports - 544-767', width: 555 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@
background-color: var(--brand-color-canvas-default);
}

// Keep the solid canvas on hover because Primer's invisible IconButton uses a
// translucent tint that lets crumbs bleed through. A currentColor stroke thickens
// the fill-based octicon glyph for hover feedback.
// Keep the solid canvas on hover because the invisible variant's translucent tint
// lets crumbs bleed through. A currentColor stroke thickens the fill-based octicon
// glyph for hover feedback.
.leftChevron:hover,
.rightChevron:hover {
background-color: var(--brand-color-canvas-default) !important;
background-color: var(--brand-color-canvas-default);
color: var(--brand-color-text-default, #000000);

svg {
Expand Down
2 changes: 1 addition & 1 deletion src/frame/components/page-header/BreadcrumbsScroller.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import type { FocusEvent } from 'react'
import cx from 'clsx'
import { IconButton } from '@primer/react'
import { ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react'

import { IconButton } from '@/frame/components/ui/IconButton'
import { useTranslation } from '@/languages/components/useTranslation'
import { Breadcrumbs } from './Breadcrumbs'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,8 @@
}

// Override primer/css color-fg-muted, whose grey clashes with brand-painted breadcrumbs.
// Keep !important because PRC's invisible IconButton variant uses .prc-Button-* rules
// that otherwise win.
.toggleIcon {
color: var(--brand-color-text-muted) !important;
color: var(--brand-color-text-muted);
}

// The In this article row sits directly beneath the breadcrumb bar. DefaultLayout
Expand Down
2 changes: 1 addition & 1 deletion src/frame/components/page-header/DocsSecondaryBar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import cx from 'clsx'
import { useRouter } from 'next/router'
import { IconButton } from '@primer/react'
import { SidebarCollapseIcon, SidebarExpandIcon } from '@primer/octicons-react'

import { IconButton } from '@/frame/components/ui/IconButton'
import { useMainContext } from '@/frame/components/context/MainContext'
import { useTranslation } from '@/languages/components/useTranslation'
import { useSidebarCollapsed } from '@/frame/components/sidebar/SidebarCollapseContext'
Expand Down
4 changes: 1 addition & 3 deletions src/frame/components/page-header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ export const Header = ({ isNarrowMenuOpen, onNarrowMenuToggle }: Props) => {
onClick={handleClick}
leadingComponent={<VersionPicker variant="header" onNavigate={closeNarrowMenu} />}
trailingComponent={
languagePickerVisible ? (
<LanguagePicker variant="header" onNavigate={closeNarrowMenu} />
) : undefined
languagePickerVisible ? <LanguagePicker onNavigate={closeNarrowMenu} /> : undefined
}
>
<SubdomainNavBar.Search
Expand Down
59 changes: 59 additions & 0 deletions src/frame/components/ui/IconButton/IconButton.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Variant and size rules sit in :where() so consumer classes override them
// without !important.
.iconButton {
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
padding: 0;
// Colour lives in the variants. Invisible's transparent border keeps an outline in forced-colors mode.
border-width: 1px;
border-style: solid;
border-radius: var(--brand-borderRadius-medium);
cursor: pointer;

&:focus-visible {
outline: 2px solid var(--brand-color-focus);
outline-offset: 2px;
}
}

:where(.iconButton[data-size="small"]) {
width: 28px;
height: 28px;
}

:where(.iconButton[data-size="medium"]) {
width: 32px;
height: 32px;
}

:where(.iconButton[data-variant="default"]) {
color: var(--brand-color-text-default);
background-color: var(--brand-button-secondary-bgColor-rest);
border-color: var(--brand-button-secondary-borderColor-rest);

&:hover {
background-color: var(--brand-button-secondary-bgColor-hover);
border-color: var(--brand-button-secondary-borderColor-hover);
}

&:active {
background-color: var(--brand-button-secondary-bgColor-active);
border-color: var(--brand-button-secondary-borderColor-active);
}
}

:where(.iconButton[data-variant="invisible"]) {
color: var(--brand-color-text-muted);
background-color: transparent;
border-color: transparent;

&:hover {
background-color: var(--brand-button-subtle-bgColor-hover);
}

&:active {
background-color: var(--brand-button-subtle-bgColor-active);
}
}
55 changes: 55 additions & 0 deletions src/frame/components/ui/IconButton/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { ButtonHTMLAttributes, ElementType } from 'react'
import cx from 'clsx'
import { Tooltip } from '@primer/react-brand'

import styles from '@/frame/components/ui/IconButton/IconButton.module.scss'

export type IconButtonProps = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'aria-label' | 'children' | 'type'
> & {
icon: ElementType
'aria-label': string
variant?: 'default' | 'invisible'
size?: 'small' | 'medium'
tooltip?: boolean
tooltipDirection?: 'n' | 'e' | 's' | 'w'
}

// With a tooltip, the tooltip text labels the button through aria-labelledby, so
// the button carries no aria-label and screen readers hear one name. Hidden buttons
// skip the tooltip because nothing can hover or focus them.
export const IconButton = ({
icon: Icon,
'aria-label': ariaLabel,
variant = 'default',
size = 'medium',
tooltip = true,
tooltipDirection = 's',
className,
...props
}: IconButtonProps) => {
const isHidden = props['aria-hidden'] === true || props['aria-hidden'] === 'true'
const withTooltip = tooltip && !isHidden

const button = (
<button
type="button"
className={cx(styles.iconButton, className)}
data-variant={variant}
data-size={size}
aria-label={withTooltip ? undefined : ariaLabel}
{...props}
>
<Icon size={16} />
</button>
)

if (!withTooltip) return button

return (
<Tooltip type="label" text={ariaLabel} direction={tooltipDirection}>
{button}
</Tooltip>
)
}
2 changes: 2 additions & 0 deletions src/frame/components/ui/IconButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { IconButton } from '@/frame/components/ui/IconButton/IconButton'
export type { IconButtonProps } from '@/frame/components/ui/IconButton/IconButton'
68 changes: 68 additions & 0 deletions src/frame/components/ui/IconButton/tests/icon-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createElement } from 'react'
import { describe, expect, test } from 'vitest'
import { renderToStaticMarkup } from 'react-dom/server'
import { load } from 'cheerio'
import { SidebarExpandIcon } from '@primer/octicons-react'

import { IconButton } from '@/frame/components/ui/IconButton'
import type { IconButtonProps } from '@/frame/components/ui/IconButton'

const LABEL = 'Collapse sidebar'

function render(props: Partial<IconButtonProps> = {}) {
const html = renderToStaticMarkup(
createElement(IconButton, { icon: SidebarExpandIcon, 'aria-label': LABEL, ...props }),
)
return load(html)
}

describe('IconButton', () => {
test('labels the button through its tooltip, without a second aria-label', () => {
const $ = render()
const button = $('button')
expect(button.attr('aria-label')).toBeUndefined()
const labelId = button.attr('aria-labelledby')
expect(labelId).toBeTruthy()
const tooltip = $(`[id="${labelId}"]`)
expect(tooltip.text()).toBe(LABEL)
expect(tooltip.attr('aria-hidden')).toBe('true')
})

test('uses aria-label and renders no tooltip when the tooltip is off', () => {
const $ = render({ tooltip: false })
expect($('button').attr('aria-label')).toBe(LABEL)
expect($('button').attr('aria-labelledby')).toBeUndefined()
expect($('div').length).toBe(0)
})

test('renders no tooltip for an aria-hidden button', () => {
const $ = render({ 'aria-hidden': true, tabIndex: -1 })
expect($('button').attr('aria-label')).toBe(LABEL)
expect($('button').attr('aria-hidden')).toBe('true')
expect($('div').length).toBe(0)
})

test('is a type=button and passes through button attributes', () => {
const $ = render({
'data-testid': 'sidebar-collapse-toggle',
'aria-expanded': true,
tabIndex: 0,
className: 'consumer',
} as Partial<IconButtonProps>)
const button = $('button')
expect(button.attr('type')).toBe('button')
expect(button.attr('data-testid')).toBe('sidebar-collapse-toggle')
expect(button.attr('aria-expanded')).toBe('true')
expect(button.attr('tabindex')).toBe('0')
expect(button.hasClass('consumer')).toBe(true)
})

test('defaults to the default variant at medium size', () => {
const $ = render()
expect($('button').attr('data-variant')).toBe('default')
expect($('button').attr('data-size')).toBe('medium')
expect(render({ variant: 'invisible', size: 'small' })('button').attr('data-size')).toBe(
'small',
)
})
})
Loading
Loading