Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/modernize-action-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

ActionMenu: Refine menu spacing, typography, icons, interaction styles, and trigger-button typography to match GitHub App.
5 changes: 5 additions & 0 deletions .changeset/modernize-text-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

TextInput: Refine typography, spacing, visuals, and interaction styles.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ storybook-static
coverage/

# Playwright
.playwright-mcp/
stats.html
blob-report
results.json
Expand Down
50 changes: 50 additions & 0 deletions e2e/components/ActionMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,56 @@ const featureFlagVariants = [
] as const

test.describe('ActionMenu', () => {
test.describe('Modernized menu', () => {
for (const id of [
'components-actionmenu-features--modernized-menu',
'components-actionmenu-features--modernized-menu-with-avatars',
]) {
for (const theme of themes) {
test(`${id} ${theme} @aat`, async ({page}) => {
await visit(page, {
id,
globals: {colorScheme: theme},
})
await expect(page.getByRole('menu')).toBeVisible()
await expect(page).toHaveNoViolations()
})
}
}

test('aligns avatars, loading indicators and submenu descriptions', async ({page}) => {
await visit(page, {id: 'components-actionmenu-features--modernized-menu-with-states'})
const menu = page.getByRole('menu')
await expect(menu.locator('[data-component="Avatar"]')).toHaveCount(3)
for (const avatar of await menu.locator('[data-component="Avatar"]').all()) {
await expect(avatar).toHaveCSS('width', '20px')
await expect(avatar).toHaveCSS('height', '20px')
}

const loading = page.getByRole('menuitem', {name: /^Loading reviewers/})
await expect(loading.locator('[data-component="Spinner"] svg')).toHaveCSS('width', '14px')
await expect(loading.locator('[data-component="Spinner"] svg')).toHaveCSS('height', '14px')

const inactive = page.getByRole('menuitem', {name: 'Request review', exact: true})
const inactiveIcon = await inactive.locator('svg').boundingBox()
const inactiveLabel = await inactive.locator('[data-component="ActionList.Item.Label"]').boundingBox()
expect(inactiveIcon!.y - inactiveLabel!.y).toBeCloseTo(3, 3)

const submenu = page.getByRole('menuitem', {name: 'More reviewers', exact: true})
await expect(submenu.locator('[data-description-variant]')).toHaveCSS('margin-right', '8px')
const chevron = await submenu.locator('[data-component="ActionList.TrailingVisual"] svg').boundingBox()
const label = await submenu.locator('[data-component="ActionList.Item.Label"]').boundingBox()
expect(chevron!.y - label!.y).toBeCloseTo(2, 3)

await submenu.focus()
await page.keyboard.press('ArrowRight')
await expect(page.getByRole('menuitem', {name: 'Design', exact: true})).toBeVisible()
await page.keyboard.press('ArrowLeft')
await expect(submenu).toBeFocused()
await expect(page.getByRole('menu')).toHaveCount(1)
})
})

for (const story of stories) {
test.describe(story.title, () => {
for (const theme of themes) {
Expand Down
16 changes: 16 additions & 0 deletions e2e/components/TextInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ import {visit} from '../test-helpers/storybook'
import {themes} from '../test-helpers/themes'

test.describe('TextInput', () => {
test.describe('Modernized accessibility', () => {
for (const theme of themes) {
test.describe(theme, () => {
for (const story of ['modernized-input', 'modernized-search', 'modernized-states']) {
test(`${story} @avt`, async ({page}) => {
await visit(page, {
id: `components-textinput-features--${story}`,
globals: {colorScheme: theme},
})
await expect(page).toHaveNoViolations()
})
}
})
}
})

test.describe('Default', () => {
for (const theme of themes) {
test.describe(theme, () => {
Expand Down
212 changes: 212 additions & 0 deletions packages/react/src/ActionList/ActionList.modernize.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
.Overlay {
min-width: calc(var(--base-size-16) * 7);
background-color: var(--bgColor-default);
background-clip: padding-box;
/* stylelint-disable primer/box-shadow -- match GitHub App's floating border shadow */
box-shadow:
0 0 0 1px color-mix(in srgb, var(--borderColor-default) 50%, transparent),
0 6px 12px -3px color-mix(in srgb, color-mix(in srgb, #000 90%, var(--bgColor-default)) 4%, transparent),
0 6px 18px 0 color-mix(in srgb, color-mix(in srgb, #000 90%, var(--bgColor-default)) 12%, transparent);
/* stylelint-enable primer/box-shadow */
}

.ActionList {
--actionList-modernize-icon-size: calc(var(--base-size-16) - var(--base-size-2));

padding: var(--base-size-4);
/* stylelint-disable-next-line primer/typography -- GitHub App's menu typography has no matching Primer token */
font-size: 13px;
/* stylelint-disable-next-line primer/typography -- match the 20px menu row line height */
line-height: var(--base-size-20);

& [data-component='ActionList.Item'] {
margin-inline: 0;
/* stylelint-disable-next-line primer/borders -- GitHub App's 8px radius has no matching Primer token */
border-radius: 8px;

& > [data-size] {
min-height: var(--base-size-28);
padding-inline: var(--base-size-8);
cursor: default;
border-radius: inherit;
}

& > [data-size='medium'] {
padding-block: var(--base-size-4);
}

&:where([data-has-description='true']) [data-component='ActionList.LeadingVisual'] {
min-height: var(--base-size-20);
height: var(--base-size-20);
align-self: start;

& > [data-component='Avatar'] {
width: var(--base-size-20);
height: var(--base-size-20);
}
}

&:where([data-inactive='true']:not([data-has-description='true'])) [data-component='ActionList.LeadingVisual'] {
margin-block-start: calc(var(--base-size-6) / 2);
align-self: start;
}

&:where([aria-haspopup]) [data-component='ActionList.Item--DividerContainer'] > :not(:last-child) {
margin-inline-end: var(--base-size-8);
}

&:where([aria-haspopup][data-has-description='true']) [data-component='ActionList.TrailingVisual'] {
min-height: 0;
height: var(--actionList-modernize-icon-size);
margin-block-start: var(--base-size-2);
}
}

& [data-component='ActionList.Item.Label'],
& [data-component='ActionList.Description'],
& [data-component='ActionList.TrailingVisual'] {
font-size: inherit;
line-height: inherit;
}

& [data-description-variant='block'] {
gap: 0;
}

& [data-has-description='true'] [data-component='ActionList.Item.Label'] {
font-weight: var(--base-text-weight-medium);
}

& [data-component='ActionList.Item--DividerContainer'] {
& > :not(:last-child) {
margin-inline-end: var(--base-size-24);
}
}

& [data-component='KeybindingHint'] {
display: inline-flex;
}

& [data-kbd-chord] {
min-width: 0;
padding: 0;
font-size: inherit;
line-height: inherit;
color: var(--fgColor-muted);
background: transparent;
border: 0;
gap: 0;

& > [aria-hidden] {
min-width: var(--actionList-modernize-icon-size);
text-align: center;
}
}

& [data-component='ActionList.LeadingVisual'] {
min-width: var(--actionList-modernize-icon-size);
min-height: 0;
height: var(--actionList-modernize-icon-size);
align-self: center;

& > :where(svg, [data-component='Avatar']) {
width: var(--actionList-modernize-icon-size);
height: var(--actionList-modernize-icon-size);
}
}

& [data-component='ActionList.Selection'] > svg,
& [data-component='ActionList.TrailingVisual'] > svg,
& [data-loading='true'] svg {
width: var(--actionList-modernize-icon-size);
height: var(--actionList-modernize-icon-size);
}

& [data-is-disabled] {
& [data-component='ActionList.Item.Label'],
& [data-component='ActionList.Description'],
& [data-component='ActionList.LeadingVisual'],
& [data-component='ActionList.TrailingVisual'],
& [data-component='ActionList.Selection'] {
color: var(--fgColor-muted);

& > svg {
color: inherit;
}
}
}

& [data-component='ActionList.Divider'] {
margin: var(--base-size-4) var(--base-size-8);
/* stylelint-disable-next-line primer/colors -- match GitHub App's subtle separator */
background: color-mix(in srgb, var(--borderColor-muted) 30%, transparent);
}

&[data-dividers='true'] {
& [data-component='ActionList.Item--DividerContainer']::before,
& [data-description-variant='inline']::before {
top: calc(-1 * var(--base-size-4));
}
}

& [data-component='ActionList.Group']:not(:first-child) {
margin-block-start: 0;
}

& [data-component='GroupHeadingWrap'] {
padding: var(--base-size-6) var(--base-size-8) var(--base-size-4);
font-weight: var(--base-text-weight-medium);
/* stylelint-disable-next-line primer/typography -- match GitHub App's 16px caption line height */
line-height: var(--base-size-16);

& > span {
font-weight: inherit;
}
}

& [data-component='ActionList.Item']:not([data-is-disabled], [data-inactive='true'], [data-loading='true']) {
@media (hover: hover) {
&:hover {
cursor: default;
box-shadow: none;
}
}

&:active {
box-shadow: none;
}

&:focus-visible {
background: transparent;
box-shadow: none;
}
}

&
[data-component='ActionList.Item']:where([data-variant='danger']):where(
:not([data-is-disabled], [data-inactive='true'], [data-loading='true'])
) {
@media (hover: hover) {
&:hover {
background: var(--bgColor-danger-muted);
}
}

&:active {
background: var(--bgColor-danger-muted);
}

&:hover,
&:active {
& [data-component='ActionList.Item.Label'],
& [data-component='ActionList.LeadingVisual'],
& [data-component='ActionList.Selection'] {
color: var(--fgColor-danger);
}

& [data-kbd-chord] {
background-color: transparent;
}
}
}
}
3 changes: 2 additions & 1 deletion packages/react/src/ActionList/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {useMergedRefs, useProvidedRefOrCreate} from '../hooks'
import {FocusKeys, useFocusZone} from '../hooks/useFocusZone'
import {clsx} from 'clsx'
import classes from './ActionList.module.css'
import modernizeClasses from './ActionList.modernize.module.css'
import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect'
import {useFeatureFlag} from '../FeatureFlags'

Expand Down Expand Up @@ -110,7 +111,7 @@ const UnwrappedList = <As extends React.ElementType = 'ul'>(
<ListContext.Provider value={listContextValue}>
{slots.heading}
<Component
className={clsx(classes.ActionList, className)}
className={clsx(classes.ActionList, container === 'ActionMenu' && modernizeClasses.ActionList, className)}
role={listRole}
aria-labelledby={ariaLabelledBy}
ref={appliedRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
color: var(--fgColor-muted);
display: inline-block;
}

.ComparisonStory {
padding: var(--base-size-16);
}
Loading
Loading