From 6713e72124f4b9e594b31d5abc7dda258940c9cc Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 25 Oct 2024 09:10:40 -0400 Subject: [PATCH 001/104] AvatarStack: Add keyboard support to `AvatarStack` (#5134) * Add keyboard support for `AvatarStack` * Add to tests, function * Add changeset * Utilize focus styles for container * Update `getInteractiveNodes` * Change name * Change case * Rework `useEffect` * Add mutation observer, updates to `hasInteractiveNodes` --- .changeset/shy-seahorses-mix.md | 5 ++ .../react/src/AvatarStack/AvatarStack.tsx | 44 ++++++++++-- .../react/src/__tests__/AvatarStack.test.tsx | 24 +++++++ .../__snapshots__/AvatarStack.test.tsx.snap | 55 +++++++++++++-- .../__tests__/hasInteractiveNodes.test.ts | 59 ++++++++++++++++ .../src/internal/utils/hasInteractiveNodes.ts | 69 +++++++++++++++++++ 6 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 .changeset/shy-seahorses-mix.md create mode 100644 packages/react/src/internal/utils/__tests__/hasInteractiveNodes.test.ts create mode 100644 packages/react/src/internal/utils/hasInteractiveNodes.ts diff --git a/.changeset/shy-seahorses-mix.md b/.changeset/shy-seahorses-mix.md new file mode 100644 index 00000000000..1819269fecf --- /dev/null +++ b/.changeset/shy-seahorses-mix.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +AvatarStack: Adds keyboard support to `AvatarStack` diff --git a/packages/react/src/AvatarStack/AvatarStack.tsx b/packages/react/src/AvatarStack/AvatarStack.tsx index f07615f7214..8430fc199b5 100644 --- a/packages/react/src/AvatarStack/AvatarStack.tsx +++ b/packages/react/src/AvatarStack/AvatarStack.tsx @@ -1,5 +1,5 @@ import {clsx} from 'clsx' -import React from 'react' +import React, {useEffect, useRef, useState} from 'react' import styled from 'styled-components' import {get} from '../constants' import Box from '../Box' @@ -12,6 +12,8 @@ import {isResponsiveValue} from '../hooks/useResponsiveValue' import {getBreakpointDeclarations} from '../utils/getBreakpointDeclarations' import {defaultSxProp} from '../utils/defaultSxProp' import type {WidthOnlyViewportRangeKeys} from '../utils/types/ViewportRangeKeys' +import {hasInteractiveNodes} from '../internal/utils/hasInteractiveNodes' +import getGlobalFocusStyles from '../internal/utils/getGlobalFocusStyles' type StyledAvatarStackWrapperProps = { count?: number @@ -30,6 +32,8 @@ const AvatarStackWrapper = styled.span` .pc-AvatarStackBody { display: flex; position: absolute; + + ${getGlobalFocusStyles('1px')} } .pc-AvatarItem { @@ -130,7 +134,8 @@ const AvatarStackWrapper = styled.span` .pc-AvatarStackBody { flex-direction: row-reverse; - &:not(.pc-AvatarStack--disableExpand):hover { + &:not(.pc-AvatarStack--disableExpand):hover, + &:not(.pc-AvatarStack--disableExpand):focus-within { .pc-AvatarItem { margin-right: ${get('space.1')}!important; margin-left: 0 !important; @@ -143,7 +148,8 @@ const AvatarStackWrapper = styled.span` } } - .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover { + .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover, + .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within { width: auto; .pc-AvatarItem { @@ -157,6 +163,8 @@ const AvatarStackWrapper = styled.span` visibility 0.2s ease-in-out, box-shadow 0.1s ease-in-out; + ${getGlobalFocusStyles('1px')} + &:first-child { margin-left: 0; } @@ -195,6 +203,9 @@ const AvatarStack = ({ className, sx: sxProp = defaultSxProp, }: AvatarStackProps) => { + const [hasInteractiveChildren, setHasInteractiveChildren] = useState(false) + const stackContainer = useRef(null) + const count = React.Children.count(children) const wrapperClassNames = clsx( { @@ -249,6 +260,25 @@ const AvatarStack = ({ ) } + useEffect(() => { + if (stackContainer.current) { + const interactiveChildren = () => { + setHasInteractiveChildren(hasInteractiveNodes(stackContainer.current)) + } + + const observer = new MutationObserver(interactiveChildren) + + observer.observe(stackContainer.current, {childList: true}) + + // Call on initial render, then call it again only if there's a mutation + interactiveChildren() + + return () => { + observer.disconnect() + } + } + }, []) + const getResponsiveAvatarSizeStyles = () => { // if there is no size set on the AvatarStack, use the `size` props of the Avatar children to set the `--avatar-stack-size` CSS variable if (!size) { @@ -279,7 +309,13 @@ const AvatarStack = ({ return ( - {transformChildren(children)} + + {transformChildren(children)} + ) } diff --git a/packages/react/src/__tests__/AvatarStack.test.tsx b/packages/react/src/__tests__/AvatarStack.test.tsx index 2807a9b76aa..9b5bfd27a4b 100644 --- a/packages/react/src/__tests__/AvatarStack.test.tsx +++ b/packages/react/src/__tests__/AvatarStack.test.tsx @@ -69,4 +69,28 @@ describe('Avatar', () => { it('respects alignRight props', () => { expect(render(rightAvatarComp)).toMatchSnapshot() }) + + it('should have a tabindex of 0 if there are no interactive children', () => { + const {container} = HTMLRender(avatarComp) + expect(container.querySelector('[tabindex="0"]')).toBeInTheDocument() + }) + + it('should not have a tabindex if there are interactive children', () => { + const {container} = HTMLRender( + + + , + ) + expect(container.querySelector('[tabindex="0"]')).not.toBeInTheDocument() + }) + + it('should not have a tabindex if disableExpand is true', () => { + const {container} = HTMLRender( + + + + , + ) + expect(container.querySelector('[tabindex="0"]')).not.toBeInTheDocument() + }) }) diff --git a/packages/react/src/__tests__/__snapshots__/AvatarStack.test.tsx.snap b/packages/react/src/__tests__/__snapshots__/AvatarStack.test.tsx.snap index f3c1df84433..cc28e33c219 100644 --- a/packages/react/src/__tests__/__snapshots__/AvatarStack.test.tsx.snap +++ b/packages/react/src/__tests__/__snapshots__/AvatarStack.test.tsx.snap @@ -23,6 +23,22 @@ exports[`Avatar respects alignRight props 1`] = ` position: absolute; } +.c0 .pc-AvatarStackBody:focus:not(:disabled) { + box-shadow: none; + outline: 2px solid var(--fgColor-accent,var(--color-accent-fg,#0969da)); + outline-offset: 1px; +} + +.c0 .pc-AvatarStackBody:focus:not(:disabled):not(:focus-visible) { + outline: solid 1px transparent; +} + +.c0 .pc-AvatarStackBody:focus-visible:not(:disabled) { + box-shadow: none; + outline: 2px solid var(--fgColor-accent,var(--color-accent-fg,#0969da)); + outline-offset: 1px; +} + .c0 .pc-AvatarItem { --avatar-size: var(--avatar-stack-size); -webkit-flex-shrink: 0; @@ -107,20 +123,24 @@ exports[`Avatar respects alignRight props 1`] = ` flex-direction: row-reverse; } -.c0.pc-AvatarStack--right .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem { +.c0.pc-AvatarStack--right .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem, +.c0.pc-AvatarStack--right .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within .pc-AvatarItem { margin-right: 4px!important; margin-left: 0 !important; } -.c0.pc-AvatarStack--right .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem:first-child { +.c0.pc-AvatarStack--right .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem:first-child, +.c0.pc-AvatarStack--right .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within .pc-AvatarItem:first-child { margin-right: 0 !important; } -.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover { +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover, +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within { width: auto; } -.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem { +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem, +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within .pc-AvatarItem { margin-left: 4px; opacity: 100%; visibility: visible; @@ -128,11 +148,32 @@ exports[`Avatar respects alignRight props 1`] = ` transition: margin 0.2s ease-in-out,opacity 0.2s ease-in-out,visibility 0.2s ease-in-out,box-shadow 0.1s ease-in-out; } -.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem box-shadow:inset 0 0 0 4px function (props) { +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem box-shadow:inset 0 0 0 4px function (props), +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within .pc-AvatarItem box-shadow:inset 0 0 0 4px function (props) { return: (0,_core.get)(props.theme,path,fallback); } -.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem:first-child { +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem:focus:not(:disabled), +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within .pc-AvatarItem:focus:not(:disabled) { + box-shadow: none; + outline: 2px solid var(--fgColor-accent,var(--color-accent-fg,#0969da)); + outline-offset: 1px; +} + +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem:focus:not(:disabled):not(:focus-visible), +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within .pc-AvatarItem:focus:not(:disabled):not(:focus-visible) { + outline: solid 1px transparent; +} + +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem:focus-visible:not(:disabled), +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within .pc-AvatarItem:focus-visible:not(:disabled) { + box-shadow: none; + outline: 2px solid var(--fgColor-accent,var(--color-accent-fg,#0969da)); + outline-offset: 1px; +} + +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):hover .pc-AvatarItem:first-child, +.c0 .pc-AvatarStackBody:not(.pc-AvatarStack--disableExpand):focus-within .pc-AvatarItem:first-child { margin-left: 0; } @@ -145,8 +186,8 @@ exports[`Avatar respects alignRight props 1`] = ` >
- { + test('if there are no interactive nodes', () => { + const node = document.createElement('div') + expect(hasInteractiveNodes(node)).toBe(false) + }) + + test('if there are interactive nodes', () => { + const node = document.createElement('div') + const button = document.createElement('button') + node.appendChild(button) + + expect(hasInteractiveNodes(node)).toBe(true) + }) + + test('if the node itself is interactive', () => { + const node = document.createElement('button') + + expect(hasInteractiveNodes(node)).toBe(false) + }) + + test('if there are nested interactive nodes', () => { + const node = document.createElement('div') + const wrapper = document.createElement('div') + const button = document.createElement('button') + const span = document.createElement('span') + wrapper.appendChild(button) + button.appendChild(span) + node.appendChild(wrapper) + + expect(hasInteractiveNodes(node)).toBe(true) + }) + + test('if the node is disabled', () => { + const node = document.createElement('button') + node.disabled = true + + expect(hasInteractiveNodes(node)).toBe(false) + }) + + test('if the child node is disabled', () => { + const node = document.createElement('div') + const button = document.createElement('button') + button.disabled = true + node.appendChild(button) + + expect(hasInteractiveNodes(node)).toBe(false) + }) + + test('if child node has tabindex', () => { + const node = document.createElement('div') + const span = document.createElement('span') + span.setAttribute('tabindex', '0') + node.appendChild(span) + + expect(hasInteractiveNodes(node)).toBe(true) + }) +}) diff --git a/packages/react/src/internal/utils/hasInteractiveNodes.ts b/packages/react/src/internal/utils/hasInteractiveNodes.ts new file mode 100644 index 00000000000..b74c52dd9fe --- /dev/null +++ b/packages/react/src/internal/utils/hasInteractiveNodes.ts @@ -0,0 +1,69 @@ +const nonValidSelectors = { + disabled: '[disabled]', + hidden: '[hidden]', + inert: '[inert]', + negativeTabIndex: '[tabindex="-1"]', +} + +const interactiveElementsSelectors = [ + `a[href]`, + `button`, + 'summary', + 'select', + 'input:not([type=hidden])', + 'textarea', + '[tabindex="0"]', + `audio[controls]`, + `video[controls]`, + `[contenteditable]`, +] + +const interactiveElements = interactiveElementsSelectors.map( + selector => `${selector}:not(${Object.values(nonValidSelectors).join('):not(')})`, +) + +/** + * Finds interactive nodes within the passed node. + * If the node itself is interactive, or children within are, it will return true. + * + * @param node - The HTML element to search for interactive nodes in. + * @param ignoreSelectors - A string of selectors to ignore when searching for interactive nodes. This is useful for + * ignoring nodes that are conditionally interactive based on the return value of the function. + * @returns {boolean | undefined} + */ +export function hasInteractiveNodes(node: HTMLElement | null, ignoreNodes?: HTMLElement[]) { + if (!node || isNonValidInteractiveNode(node)) return false + + // We only need to confirm if at least one interactive node exists. + // If one does exist, we can abort early. + + const nodesToIgnore = ignoreNodes ? [node, ...ignoreNodes] : [node] + const interactiveNodes = findInteractiveChildNodes(node, nodesToIgnore) + + return Boolean(interactiveNodes) +} + +function isNonValidInteractiveNode(node: HTMLElement) { + const nodeStyle = getComputedStyle(node) + const isNonInteractive = node.matches('[disabled], [hidden], [inert]') + const isHiddenVisually = nodeStyle.display === 'none' || nodeStyle.visibility === 'hidden' + + return isNonInteractive || isHiddenVisually +} + +function findInteractiveChildNodes(node: HTMLElement | null, ignoreNodes: HTMLElement[]) { + if (!node) return + + const ignoreSelector = ignoreNodes.find(elem => elem === node) + const isNotValidNode = isNonValidInteractiveNode(node) + + if (node.matches(interactiveElements.join(', ')) && !ignoreSelector && !isNotValidNode) { + return node + } + + for (const child of node.children) { + const interactiveNode = findInteractiveChildNodes(child as HTMLElement, ignoreNodes) + + if (interactiveNode) return true + } +} From c9e68d2fb010e30fbc0a925282a6cf790176937c Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Mon, 28 Oct 2024 13:42:02 -0400 Subject: [PATCH 002/104] AvatarStack: Adds back empty space in `AvatarStack` (#5181) * adds back empty space in `AvatarStack` * Update snapshot --- packages/react/src/AvatarStack/AvatarStack.tsx | 1 + .../react/src/__tests__/__snapshots__/AvatarStack.test.tsx.snap | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/react/src/AvatarStack/AvatarStack.tsx b/packages/react/src/AvatarStack/AvatarStack.tsx index 8430fc199b5..ccd87f1e24f 100644 --- a/packages/react/src/AvatarStack/AvatarStack.tsx +++ b/packages/react/src/AvatarStack/AvatarStack.tsx @@ -314,6 +314,7 @@ const AvatarStack = ({ tabIndex={!hasInteractiveChildren && !disableExpand ? 0 : undefined} ref={stackContainer} > + {' '} {transformChildren(children)} diff --git a/packages/react/src/__tests__/__snapshots__/AvatarStack.test.tsx.snap b/packages/react/src/__tests__/__snapshots__/AvatarStack.test.tsx.snap index cc28e33c219..8f68da6c108 100644 --- a/packages/react/src/__tests__/__snapshots__/AvatarStack.test.tsx.snap +++ b/packages/react/src/__tests__/__snapshots__/AvatarStack.test.tsx.snap @@ -188,6 +188,7 @@ exports[`Avatar respects alignRight props 1`] = ` className="pc-AvatarStackBody" tabIndex={0} > + Date: Mon, 28 Oct 2024 14:41:22 -0400 Subject: [PATCH 003/104] Overlay: Add proper roles w/ keyboard expectations to stories (#5175) * Add proper roles w/ keyboard expectations to stories * Make prop optional --- .../src/Overlay/Overlay.features.stories.tsx | 108 ++++++++++++++++-- 1 file changed, 99 insertions(+), 9 deletions(-) diff --git a/packages/react/src/Overlay/Overlay.features.stories.tsx b/packages/react/src/Overlay/Overlay.features.stories.tsx index ac35e8bfc5a..340181ab53c 100644 --- a/packages/react/src/Overlay/Overlay.features.stories.tsx +++ b/packages/react/src/Overlay/Overlay.features.stories.tsx @@ -1,6 +1,6 @@ import React, {useState, useRef, useCallback} from 'react' import type {Meta} from '@storybook/react' -import {TriangleDownIcon, PlusIcon, IssueDraftIcon} from '@primer/octicons-react' +import {TriangleDownIcon, PlusIcon, IssueDraftIcon, XIcon} from '@primer/octicons-react' import { Overlay, ButtonGroup, @@ -16,8 +16,10 @@ import { Label, ActionList, ActionMenu, + useFocusTrap, } from '..' import type {AnchorSide} from '@primer/behaviors' +import type {AriaRole} from '../utils/types' import {Tooltip} from '../TooltipV2' export default { @@ -25,6 +27,7 @@ export default { component: Overlay, args: { anchorSide: 'inside-top', + role: 'dialog', }, argTypes: { anchorSide: { @@ -43,16 +46,22 @@ export default { 'outside-right', ], }, + role: { + type: 'string', + }, }, } as Meta interface OverlayProps { - anchorSide: AnchorSide + anchorSide?: AnchorSide + role?: AriaRole + right?: boolean } export const DropdownOverlay = ({anchorSide}: OverlayProps) => { const [isOpen, setIsOpen] = useState(false) const buttonRef = useRef(null) + return ( <> + )} + open={open} + onOpenChange={setOpen} + items={filteredItems} + selected={selected} + onSelectedChange={setSelected} + onFilterChange={setFilter} + /> + + ) +} + export const WithLeadingVisual = () => ( diff --git a/packages/react/src/FormControl/FormControl.tsx b/packages/react/src/FormControl/FormControl.tsx index 257163c753a..a6ccd15c858 100644 --- a/packages/react/src/FormControl/FormControl.tsx +++ b/packages/react/src/FormControl/FormControl.tsx @@ -4,6 +4,7 @@ import Box from '../Box' import Checkbox from '../Checkbox' import Radio from '../Radio' import Select from '../Select' +import {SelectPanel} from '../SelectPanel' import TextInput from '../TextInput' import TextInputWithTokens from '../TextInputWithTokens' import Textarea from '../Textarea' @@ -50,7 +51,16 @@ const FormControl = React.forwardRef( leadingVisual: FormControlLeadingVisual, validation: FormControlValidation, }) - const expectedInputComponents = [Autocomplete, Checkbox, Radio, Select, TextInput, TextInputWithTokens, Textarea] + const expectedInputComponents = [ + Autocomplete, + Checkbox, + Radio, + Select, + TextInput, + TextInputWithTokens, + Textarea, + SelectPanel, + ] const choiceGroupContext = useContext(CheckboxOrRadioGroupContext) const disabled = choiceGroupContext.disabled || disabledProp const id = useId(idProp) From 573ae5168879998946ba1aa3e21af8ddf54351af Mon Sep 17 00:00:00 2001 From: Randall Krauskopf <104226843+randall-krauskopf@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:43:50 -0500 Subject: [PATCH 008/104] feat(Spinner): Convert Spinner to CSS module behind feature flag (#5188) * Convert spinner component to CSS modules Co-authored-by: Hussam Ghazzi Co-authored-by: Josh Black Co-authored-by: Jon Rohan * add changeset * update snapshots * patch -> minor --------- Co-authored-by: Hussam Ghazzi Co-authored-by: Josh Black Co-authored-by: Jon Rohan --- .changeset/hungry-avocados-remember.md | 5 + .../react/src/Spinner/Spinner.dev.stories.tsx | 10 + packages/react/src/Spinner/Spinner.module.css | 13 + packages/react/src/Spinner/Spinner.tsx | 26 +- .../__snapshots__/Autocomplete.test.tsx.snap | 17 +- .../__snapshots__/TextInput.test.tsx.snap | 294 ++--- .../TextInputWithTokens.test.tsx.snap | 1014 ++++++++--------- 7 files changed, 625 insertions(+), 754 deletions(-) create mode 100644 .changeset/hungry-avocados-remember.md create mode 100644 packages/react/src/Spinner/Spinner.dev.stories.tsx create mode 100644 packages/react/src/Spinner/Spinner.module.css diff --git a/.changeset/hungry-avocados-remember.md b/.changeset/hungry-avocados-remember.md new file mode 100644 index 00000000000..ffe0993061d --- /dev/null +++ b/.changeset/hungry-avocados-remember.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Migrated `Spinner` component to use support CSS modules diff --git a/packages/react/src/Spinner/Spinner.dev.stories.tsx b/packages/react/src/Spinner/Spinner.dev.stories.tsx new file mode 100644 index 00000000000..4faf30a1d02 --- /dev/null +++ b/packages/react/src/Spinner/Spinner.dev.stories.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import type {Meta} from '@storybook/react' +import Spinner from './Spinner' + +export default { + title: 'Components/Spinner/Dev', + component: Spinner, +} as Meta + +export const Default = () => diff --git a/packages/react/src/Spinner/Spinner.module.css b/packages/react/src/Spinner/Spinner.module.css new file mode 100644 index 00000000000..0de12278040 --- /dev/null +++ b/packages/react/src/Spinner/Spinner.module.css @@ -0,0 +1,13 @@ +.Box { + display: inline-flex; +} + +@keyframes rotate-keyframes { + 100% { + transform: rotate(360deg); + } +} + +.SpinnerAnimation { + animation: rotate-keyframes 1s linear infinite; +} diff --git a/packages/react/src/Spinner/Spinner.tsx b/packages/react/src/Spinner/Spinner.tsx index 63cd0d33304..6292234b6fe 100644 --- a/packages/react/src/Spinner/Spinner.tsx +++ b/packages/react/src/Spinner/Spinner.tsx @@ -3,8 +3,10 @@ import styled from 'styled-components' import sx, {type SxProp} from '../sx' import {VisuallyHidden} from '../VisuallyHidden' import type {HTMLDataAttributes} from '../internal/internal-types' -import Box from '../Box' import {useId} from '../hooks' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './Spinner.module.css' +import Box from '../Box' const sizeMap = { small: '16px', @@ -20,6 +22,7 @@ export type SpinnerProps = { /** @deprecated Use `srText` instead. */ 'aria-label'?: string className?: string + style?: React.CSSProperties } & HTMLDataAttributes & SxProp @@ -28,6 +31,7 @@ function Spinner({ srText = 'Loading', 'aria-label': ariaLabel, className, + style, ...props }: SpinnerProps) { const size = sizeMap[sizeKey] @@ -36,7 +40,7 @@ function Spinner({ return ( /* inline-flex removes the extra line height */ - + {hasHiddenLabel ? {srText} : null} - + ) } -const StyledSpinner = styled(Spinner)` +const StyledComponentSpinner = styled(Spinner)` @keyframes rotate-keyframes { 100% { transform: rotate(360deg); @@ -82,6 +87,19 @@ const StyledSpinner = styled(Spinner)` ${sx} ` +function StyledSpinner({sx, ...props}: SpinnerProps) { + const enabled = useFeatureFlag('primer_react_css_modules_team') + if (enabled) { + if (sx) { + return + } + + return + } + + return +} + StyledSpinner.displayName = 'Spinner' export default StyledSpinner diff --git a/packages/react/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap b/packages/react/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap index 9637ecc100e..c3dd83b8058 100644 --- a/packages/react/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +++ b/packages/react/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap @@ -327,14 +327,7 @@ exports[`snapshots renders a loading state 1`] = ` justify-content: center; } -.c2 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - -.c4:not(:focus):not(:active):not(:focus-within) { +.c3:not(:focus):not(:active):not(:focus-within) { -webkit-clip-path: inset(50%); clip-path: inset(50%); height: 1px; @@ -344,7 +337,7 @@ exports[`snapshots renders a loading state 1`] = ` width: 1px; } -.c3 { +.c2 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; } @@ -374,12 +367,12 @@ exports[`snapshots renders a loading state 1`] = ` display="flex" > Loading diff --git a/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap b/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap index 2c8c4fe8878..2c67fb764da 100644 --- a/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap +++ b/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -4038,13 +4038,6 @@ exports[`TextInput renders with a loading indicator 1`] = ` } .c5 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - -.c6 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; visibility: visible; @@ -4212,11 +4205,11 @@ exports[`TextInput renders with a loading indicator 1`] = ` id=":r2n:" > Loading @@ -4481,11 +4467,11 @@ exports[`TextInput renders with a loading indicator 1`] = ` id=":r2r:" >
Loading @@ -5008,11 +4980,11 @@ exports[`TextInput renders with a loading indicator 1`] = ` id=":r34:" > Loading @@ -5316,11 +5281,11 @@ exports[`TextInput renders with a loading indicator 1`] = ` id=":r39:" > Loading @@ -5624,11 +5582,11 @@ exports[`TextInput renders with a loading indicator 1`] = ` id=":r3e:" > Loading @@ -6160,7 +6104,7 @@ exports[`TextInput renders with a loading indicator 1`] = ` id=":r3n:" >
Loading @@ -6774,7 +6704,7 @@ exports[`TextInput renders with a loading indicator 1`] = ` id=":r40:" >
Loading @@ -7121,7 +7044,7 @@ exports[`TextInput renders with a loading indicator 1`] = ` id=":r45:" >
Loading @@ -7475,7 +7391,7 @@ exports[`TextInput renders with a loading indicator 1`] = ` id=":r4a:" >
, - .c5 { + .c4 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -7066,7 +7059,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` flex-grow: 1; } -.c5 > * { +.c4 > * { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -7074,7 +7067,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` margin-bottom: 0.25rem; } -.c6 { +.c5 { -webkit-order: 1; -ms-flex-order: 1; order: 1; @@ -7093,25 +7086,18 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` } .c3 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - -.c4 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; visibility: visible; } -.c12 { +.c11 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; visibility: hidden; } -.c10 { +.c9 { position: absolute; width: 1px; height: 1px; @@ -7224,7 +7210,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 12px; } -.c7 { +.c6 { border: 0; font-size: inherit; font-family: inherit; @@ -7235,11 +7221,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` height: 100%; } -.c7:focus { +.c6:focus { outline: 0; } -.c8 { +.c7 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -7272,13 +7258,13 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c8:hover { +.c7:hover { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); box-shadow: var(--shadow-resting-medium,var(--color-shadow-medium,0 3px 6px rgba(140,149,159,0.15))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } -.c11 { +.c10 { background-color: transparent; font-family: inherit; color: currentColor; @@ -7318,16 +7304,16 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` width: 32px; } -.c11:hover, -.c11:focus { +.c10:hover, +.c10:focus { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); } -.c11:active { +.c10:active { background-color: var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))); } -.c9 { +.c8 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -7352,11 +7338,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` text-decoration: none; } -.c9:is(a,button,[tabIndex='0']) { +.c8:is(a,button,[tabIndex='0']) { cursor: pointer; } -.c9:is(a,button,[tabIndex='0']):after { +.c8:is(a,button,[tabIndex='0']):after { content: ''; position: absolute; left: 0; @@ -7388,11 +7374,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` display="flex" >
zero (press backspace or delete to remove) one (press backspace or delete to remove) two (press backspace or delete to remove) three (press backspace or delete to remove) four (press backspace or delete to remove) five (press backspace or delete to remove) six (press backspace or delete to remove) seven (press backspace or delete to remove)
zero (press backspace or delete to remove) one (press backspace or delete to remove) two (press backspace or delete to remove) three (press backspace or delete to remove) four (press backspace or delete to remove) five (press backspace or delete to remove) six (press backspace or delete to remove) seven (press backspace or delete to remove) , - .c6 { + .c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -9570,7 +9542,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` flex-grow: 1; } -.c6 > * { +.c5 > * { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -9578,7 +9550,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` margin-bottom: 0.25rem; } -.c7 { +.c6 { -webkit-order: 1; -ms-flex-order: 1; order: 1; @@ -9596,24 +9568,17 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` position: relative; } -.c4 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - .c3 { visibility: hidden; } -.c13 { +.c12 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; visibility: hidden; } -.c5 { +.c4 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; position: absolute; @@ -9624,7 +9589,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` left: 0; } -.c11 { +.c10 { position: absolute; width: 1px; height: 1px; @@ -9737,7 +9702,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 12px; } -.c8 { +.c7 { border: 0; font-size: inherit; font-family: inherit; @@ -9748,11 +9713,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` height: 100%; } -.c8:focus { +.c7:focus { outline: 0; } -.c9 { +.c8 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -9785,13 +9750,13 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c9:hover { +.c8:hover { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); box-shadow: var(--shadow-resting-medium,var(--color-shadow-medium,0 3px 6px rgba(140,149,159,0.15))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } -.c12 { +.c11 { background-color: transparent; font-family: inherit; color: currentColor; @@ -9831,16 +9796,16 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` width: 32px; } -.c12:hover, -.c12:focus { +.c11:hover, +.c11:focus { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); } -.c12:active { +.c11:active { background-color: var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))); } -.c10 { +.c9 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -9865,11 +9830,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` text-decoration: none; } -.c10:is(a,button,[tabIndex='0']) { +.c9:is(a,button,[tabIndex='0']) { cursor: pointer; } -.c10:is(a,button,[tabIndex='0']):after { +.c9:is(a,button,[tabIndex='0']):after { content: ''; position: absolute; left: 0; @@ -9926,11 +9891,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = `
zero (press backspace or delete to remove) one (press backspace or delete to remove) two (press backspace or delete to remove) three (press backspace or delete to remove) four (press backspace or delete to remove) five (press backspace or delete to remove) six (press backspace or delete to remove) seven (press backspace or delete to remove) , - .c6 { + .c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -10447,7 +10412,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` flex-grow: 1; } -.c6 > * { +.c5 > * { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -10455,7 +10420,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` margin-bottom: 0.25rem; } -.c7 { +.c6 { -webkit-order: 1; -ms-flex-order: 1; order: 1; @@ -10473,24 +10438,17 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` position: relative; } -.c4 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - .c3 { visibility: visible; } -.c13 { +.c12 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; visibility: visible; } -.c5 { +.c4 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; position: absolute; @@ -10501,7 +10459,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` left: 0; } -.c11 { +.c10 { position: absolute; width: 1px; height: 1px; @@ -10614,7 +10572,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c8 { +.c7 { border: 0; font-size: inherit; font-family: inherit; @@ -10625,11 +10583,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` height: 100%; } -.c8:focus { +.c7:focus { outline: 0; } -.c9 { +.c8 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -10662,13 +10620,13 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c9:hover { +.c8:hover { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); box-shadow: var(--shadow-resting-medium,var(--color-shadow-medium,0 3px 6px rgba(140,149,159,0.15))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } -.c12 { +.c11 { background-color: transparent; font-family: inherit; color: currentColor; @@ -10708,16 +10666,16 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` width: 32px; } -.c12:hover, -.c12:focus { +.c11:hover, +.c11:focus { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); } -.c12:active { +.c11:active { background-color: var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))); } -.c10 { +.c9 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -10742,11 +10700,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` text-decoration: none; } -.c10:is(a,button,[tabIndex='0']) { +.c9:is(a,button,[tabIndex='0']) { cursor: pointer; } -.c10:is(a,button,[tabIndex='0']):after { +.c9:is(a,button,[tabIndex='0']):after { content: ''; position: absolute; left: 0; @@ -10803,11 +10761,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = `
zero (press backspace or delete to remove) one (press backspace or delete to remove) two (press backspace or delete to remove) three (press backspace or delete to remove) four (press backspace or delete to remove) five (press backspace or delete to remove) six (press backspace or delete to remove) seven (press backspace or delete to remove)
, - .c5 { + .c4 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -12152,7 +12103,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` flex-grow: 1; } -.c5 > * { +.c4 > * { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -12160,7 +12111,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` margin-bottom: 0.25rem; } -.c6 { +.c5 { -webkit-order: 1; -ms-flex-order: 1; order: 1; @@ -12178,24 +12129,17 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` position: relative; } -.c3 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - -.c12 { +.c11 { visibility: visible; } -.c4 { +.c3 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; visibility: visible; } -.c13 { +.c12 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; position: absolute; @@ -12206,7 +12150,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` right: 0; } -.c10 { +.c9 { position: absolute; width: 1px; height: 1px; @@ -12319,7 +12263,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c7 { +.c6 { border: 0; font-size: inherit; font-family: inherit; @@ -12330,11 +12274,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` height: 100%; } -.c7:focus { +.c6:focus { outline: 0; } -.c8 { +.c7 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -12367,13 +12311,13 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c8:hover { +.c7:hover { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); box-shadow: var(--shadow-resting-medium,var(--color-shadow-medium,0 3px 6px rgba(140,149,159,0.15))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } -.c11 { +.c10 { background-color: transparent; font-family: inherit; color: currentColor; @@ -12413,16 +12357,16 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` width: 32px; } -.c11:hover, -.c11:focus { +.c10:hover, +.c10:focus { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); } -.c11:active { +.c10:active { background-color: var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))); } -.c9 { +.c8 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -12447,11 +12391,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` text-decoration: none; } -.c9:is(a,button,[tabIndex='0']) { +.c8:is(a,button,[tabIndex='0']) { cursor: pointer; } -.c9:is(a,button,[tabIndex='0']):after { +.c8:is(a,button,[tabIndex='0']):after { content: ''; position: absolute; left: 0; @@ -12483,11 +12427,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` display="flex" >
zero (press backspace or delete to remove) one (press backspace or delete to remove) two (press backspace or delete to remove) three (press backspace or delete to remove) four (press backspace or delete to remove) five (press backspace or delete to remove) six (press backspace or delete to remove) seven (press backspace or delete to remove)
, - .c6 { + .c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -13857,7 +13794,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` flex-grow: 1; } -.c6 > * { +.c5 > * { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -13865,7 +13802,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` margin-bottom: 0.25rem; } -.c7 { +.c6 { -webkit-order: 1; -ms-flex-order: 1; order: 1; @@ -13883,22 +13820,15 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` position: relative; } -.c4 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - .c3 { visibility: hidden; } -.c13 { +.c12 { visibility: visible; } -.c5 { +.c4 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; position: absolute; @@ -13909,7 +13839,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` left: 0; } -.c14 { +.c13 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; position: absolute; @@ -13920,7 +13850,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` right: 0; } -.c11 { +.c10 { position: absolute; width: 1px; height: 1px; @@ -14040,7 +13970,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c8 { +.c7 { border: 0; font-size: inherit; font-family: inherit; @@ -14051,11 +13981,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` height: 100%; } -.c8:focus { +.c7:focus { outline: 0; } -.c9 { +.c8 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -14086,13 +14016,13 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c9:hover { +.c8:hover { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); box-shadow: var(--shadow-resting-medium,var(--color-shadow-medium,0 3px 6px rgba(140,149,159,0.15))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } -.c12 { +.c11 { background-color: transparent; font-family: inherit; color: currentColor; @@ -14132,16 +14062,16 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` width: 16px; } -.c12:hover, -.c12:focus { +.c11:hover, +.c11:focus { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); } -.c12:active { +.c11:active { background-color: var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))); } -.c10 { +.c9 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -14166,11 +14096,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` text-decoration: none; } -.c10:is(a,button,[tabIndex='0']) { +.c9:is(a,button,[tabIndex='0']) { cursor: pointer; } -.c10:is(a,button,[tabIndex='0']):after { +.c9:is(a,button,[tabIndex='0']):after { content: ''; position: absolute; left: 0; @@ -14227,11 +14157,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = `
zero (press backspace or delete to remove) one (press backspace or delete to remove) two (press backspace or delete to remove) three (press backspace or delete to remove) four (press backspace or delete to remove) five (press backspace or delete to remove) six (press backspace or delete to remove) seven (press backspace or delete to remove) , - .c6 { + .c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -14778,7 +14708,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` flex-grow: 1; } -.c6 > * { +.c5 > * { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -14786,7 +14716,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` margin-bottom: 0.25rem; } -.c7 { +.c6 { -webkit-order: 1; -ms-flex-order: 1; order: 1; @@ -14804,22 +14734,15 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` position: relative; } -.c4 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - .c3 { visibility: hidden; } -.c13 { +.c12 { visibility: visible; } -.c5 { +.c4 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; position: absolute; @@ -14830,7 +14753,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` left: 0; } -.c14 { +.c13 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; position: absolute; @@ -14841,7 +14764,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` right: 0; } -.c11 { +.c10 { position: absolute; width: 1px; height: 1px; @@ -14954,7 +14877,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c8 { +.c7 { border: 0; font-size: inherit; font-family: inherit; @@ -14965,11 +14888,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` height: 100%; } -.c8:focus { +.c7:focus { outline: 0; } -.c9 { +.c8 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -15002,13 +14925,13 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c9:hover { +.c8:hover { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); box-shadow: var(--shadow-resting-medium,var(--color-shadow-medium,0 3px 6px rgba(140,149,159,0.15))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } -.c12 { +.c11 { background-color: transparent; font-family: inherit; color: currentColor; @@ -15048,16 +14971,16 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` width: 32px; } -.c12:hover, -.c12:focus { +.c11:hover, +.c11:focus { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); } -.c12:active { +.c11:active { background-color: var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))); } -.c10 { +.c9 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -15082,11 +15005,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` text-decoration: none; } -.c10:is(a,button,[tabIndex='0']) { +.c9:is(a,button,[tabIndex='0']) { cursor: pointer; } -.c10:is(a,button,[tabIndex='0']):after { +.c9:is(a,button,[tabIndex='0']):after { content: ''; position: absolute; left: 0; @@ -15143,11 +15066,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = `
zero (press backspace or delete to remove) one (press backspace or delete to remove) two (press backspace or delete to remove) three (press backspace or delete to remove) four (press backspace or delete to remove) five (press backspace or delete to remove) six (press backspace or delete to remove) seven (press backspace or delete to remove) , - .c6 { + .c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -15694,7 +15617,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` flex-grow: 1; } -.c6 > * { +.c5 > * { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -15702,7 +15625,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` margin-bottom: 0.25rem; } -.c7 { +.c6 { -webkit-order: 1; -ms-flex-order: 1; order: 1; @@ -15720,14 +15643,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` position: relative; } -.c4 { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; -} - -.c13 { +.c12 { visibility: hidden; } @@ -15735,7 +15651,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` visibility: visible; } -.c5 { +.c4 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; position: absolute; @@ -15746,7 +15662,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` left: 0; } -.c14 { +.c13 { -webkit-animation: rotate-keyframes 1s linear infinite; animation: rotate-keyframes 1s linear infinite; position: absolute; @@ -15757,7 +15673,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` right: 0; } -.c11 { +.c10 { position: absolute; width: 1px; height: 1px; @@ -15870,7 +15786,7 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c8 { +.c7 { border: 0; font-size: inherit; font-family: inherit; @@ -15881,11 +15797,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` height: 100%; } -.c8:focus { +.c7:focus { outline: 0; } -.c9 { +.c8 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -15916,13 +15832,13 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` padding-right: 0; } -.c9:hover { +.c8:hover { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); box-shadow: var(--shadow-resting-medium,var(--color-shadow-medium,0 3px 6px rgba(140,149,159,0.15))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } -.c12 { +.c11 { background-color: transparent; font-family: inherit; color: currentColor; @@ -15962,16 +15878,16 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` width: 24px; } -.c12:hover, -.c12:focus { +.c11:hover, +.c11:focus { background-color: var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2))); } -.c12:active { +.c11:active { background-color: var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))); } -.c10 { +.c9 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -15996,11 +15912,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = ` text-decoration: none; } -.c10:is(a,button,[tabIndex='0']) { +.c9:is(a,button,[tabIndex='0']) { cursor: pointer; } -.c10:is(a,button,[tabIndex='0']):after { +.c9:is(a,button,[tabIndex='0']):after { content: ''; position: absolute; left: 0; @@ -16057,11 +15973,11 @@ exports[`TextInputWithTokens renders with a loading indicator 1`] = `
zero (press backspace or delete to remove) one (press backspace or delete to remove) two (press backspace or delete to remove) three (press backspace or delete to remove) four (press backspace or delete to remove) five (press backspace or delete to remove) six (press backspace or delete to remove) seven (press backspace or delete to remove)
diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.stories.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.stories.tsx
index 5a7dcbf50da..84ec0727c24 100644
--- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.stories.tsx
+++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.stories.tsx
@@ -2,7 +2,7 @@ import React, {useState} from 'react'
 import type {Args, Meta} from '@storybook/react'
 import {LocationIcon, RepoIcon} from '@primer/octicons-react'
 
-import {Avatar, Text} from '..'
+import {Avatar, Link, Text} from '..'
 import {AnchoredOverlay} from '../AnchoredOverlay'
 import {Button} from '../Button'
 import Octicon from '../Octicon'
@@ -23,7 +23,9 @@ const hoverCard = (
     
       monalisa
       
-        Monalisa Octocat
+        
+          Monalisa Octocat
+        
       
     
     Former beach cat and champion swimmer. Now your friendly octapus with a normal face.
@@ -51,6 +53,8 @@ export const Default = () => {
       onOpen={() => setOpen(true)}
       onClose={() => setOpen(false)}
       renderAnchor={props => }
+      overlayProps={{role: 'dialog', 'aria-modal': true, 'aria-label': 'User Card Overlay'}}
+      focusZoneSettings={{disabled: true}}
     >
       {hoverCard}
     
@@ -74,8 +78,14 @@ export const Playground = (args: Args) => {
       width={args.width}
       height={args.height}
       renderAnchor={props => }
-      overlayProps={args.portalContainerName}
+      overlayProps={{
+        ...args.portalContainerName,
+        role: 'dialog',
+        'aria-modal': true,
+        'aria-label': 'User Card Overlay',
+      }}
       side={args.side}
+      focusZoneSettings={{disabled: true}}
     >
       {hoverCard}
     

From 2ab7b9e649cc2f6a1c290bdbfcf48d3e635d998f Mon Sep 17 00:00:00 2001
From: Jon Rohan 
Date: Wed, 30 Oct 2024 12:15:20 -0700
Subject: [PATCH 011/104] feat(Radio): Convert Radio to css modules behind
 feature flag (#5187)

* Convert radio to CSS modules

Co-authored-by: Josh Black 
Co-authored-by: Randall Krauskopf 
Co-authored-by: Hussam Ghazzi 

* Create fifty-suns-smoke.md

* Update CSS selector for checked state

---------

Co-authored-by: Josh Black 
Co-authored-by: Randall Krauskopf 
Co-authored-by: Hussam Ghazzi 
---
 .changeset/fifty-suns-smoke.md                |  5 ++
 .../react/src/Radio/Radio.dev.stories.tsx     | 18 +++++
 packages/react/src/Radio/Radio.module.css     | 34 ++++++++
 packages/react/src/Radio/Radio.tsx            | 77 ++++++++++++-------
 4 files changed, 108 insertions(+), 26 deletions(-)
 create mode 100644 .changeset/fifty-suns-smoke.md
 create mode 100644 packages/react/src/Radio/Radio.dev.stories.tsx
 create mode 100644 packages/react/src/Radio/Radio.module.css

diff --git a/.changeset/fifty-suns-smoke.md b/.changeset/fifty-suns-smoke.md
new file mode 100644
index 00000000000..749aa264863
--- /dev/null
+++ b/.changeset/fifty-suns-smoke.md
@@ -0,0 +1,5 @@
+---
+"@primer/react": minor
+---
+
+Convert Radio to css modules behind feature flag
diff --git a/packages/react/src/Radio/Radio.dev.stories.tsx b/packages/react/src/Radio/Radio.dev.stories.tsx
new file mode 100644
index 00000000000..56dcf214241
--- /dev/null
+++ b/packages/react/src/Radio/Radio.dev.stories.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import {Box, FormControl, Radio} from '..'
+
+export default {
+  title: 'Components/Radio/Dev',
+  component: Radio,
+}
+
+export const SxProp = () => {
+  return (
+    
+      
+        
+        Label
+      
+    
+  )
+}
diff --git a/packages/react/src/Radio/Radio.module.css b/packages/react/src/Radio/Radio.module.css
new file mode 100644
index 00000000000..2032cb8a697
--- /dev/null
+++ b/packages/react/src/Radio/Radio.module.css
@@ -0,0 +1,34 @@
+.Radio {
+  border-radius: var(--borderRadius-full, 100vh);
+  transition:
+    background-color,
+    border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); /* checked -> unchecked - add 120ms delay to fully see animation-out */
+
+  &:where(:checked) {
+    /* stylelint-disable-next-line primer/colors */
+    background-color: var(--control-checked-fgColor-rest);
+
+    /* using bgColor here to avoid a border change in dark high contrast */
+    /* stylelint-disable-next-line primer/colors */
+    border-color: var(--control-checked-bgColor-rest);
+    border-width: var(--borderWidth-thicker);
+
+    &:disabled {
+      cursor: not-allowed;
+      /* stylelint-disable-next-line primer/colors */
+      background-color: var(--fgColor-muted);
+      /* stylelint-disable-next-line primer/colors */
+      border-color: var(--fgColor-muted);
+    }
+  }
+
+  &:focus,
+  &:focus-within {
+    @mixin focusOutline 2px;
+  }
+
+  @media (forced-colors: active) {
+    background-color: canvastext;
+    border-color: canvastext;
+  }
+}
diff --git a/packages/react/src/Radio/Radio.tsx b/packages/react/src/Radio/Radio.tsx
index 4ddcb346d5d..6040ad4fd20 100644
--- a/packages/react/src/Radio/Radio.tsx
+++ b/packages/react/src/Radio/Radio.tsx
@@ -8,6 +8,11 @@ import {RadioGroupContext} from '../RadioGroup/RadioGroup'
 import getGlobalFocusStyles from '../internal/utils/getGlobalFocusStyles'
 import {get} from '../constants'
 import {sharedCheckboxAndRadioStyles} from '../internal/utils/sharedCheckboxAndRadioStyles'
+import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
+import {useFeatureFlag} from '../FeatureFlags'
+import {clsx} from 'clsx'
+import classes from './Radio.module.css'
+import sharedClasses from '../Checkbox/shared.module.css'
 
 export type RadioProps = {
   /**
@@ -42,47 +47,63 @@ export type RadioProps = {
 } & InputHTMLAttributes &
   SxProp
 
-const StyledRadio = styled.input`
-  ${sharedCheckboxAndRadioStyles};
-  border-radius: var(--borderRadius-full, 100vh);
-  transition:
-    background-color,
-    border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); /* checked -> unchecked - add 120ms delay to fully see animation-out */
+const StyledRadio = toggleStyledComponent(
+  'primer_react_css_modules_team',
+  'input',
+  styled.input`
+    ${sharedCheckboxAndRadioStyles};
+    border-radius: var(--borderRadius-full, 100vh);
+    transition:
+      background-color,
+      border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); /* checked -> unchecked - add 120ms delay to fully see animation-out */
 
-  &:checked {
-    border-width: var(--base-size-4, 4px);
-    border-color: var(
-      --control-checked-bgColor-rest,
-      ${get('colors.accent.fg')}
-    ); /* using bgColor here to avoid a border change in dark high contrast */
-    background-color: var(--control-checked-fgColor-rest, ${get('colors.fg.onEmphasis')});
+    &:checked {
+      border-width: var(--base-size-4, 4px);
+      border-color: var(
+        --control-checked-bgColor-rest,
+        ${get('colors.accent.fg')}
+      ); /* using bgColor here to avoid a border change in dark high contrast */
+      background-color: var(--control-checked-fgColor-rest, ${get('colors.fg.onEmphasis')});
 
-    &:disabled {
-      cursor: not-allowed;
-      border-color: ${get('colors.fg.muted')};
-      background-color: ${get('colors.fg.muted')};
+      &:disabled {
+        cursor: not-allowed;
+        border-color: ${get('colors.fg.muted')};
+        background-color: ${get('colors.fg.muted')};
+      }
     }
-  }
 
-  ${getGlobalFocusStyles()};
+    ${getGlobalFocusStyles()};
 
-  @media (forced-colors: active) {
-    background-color: canvastext;
-    border-color: canvastext;
-  }
+    @media (forced-colors: active) {
+      background-color: canvastext;
+      border-color: canvastext;
+    }
 
-  ${sx}
-`
+    ${sx}
+  `,
+)
 
 /**
  * An accessible, native radio component for selecting one option from a list.
  */
 const Radio = React.forwardRef(
   (
-    {checked, disabled, name: nameProp, onChange, sx: sxProp, required, validationStatus, value, ...rest}: RadioProps,
+    {
+      checked,
+      disabled,
+      name: nameProp,
+      onChange,
+      sx: sxProp,
+      required,
+      validationStatus,
+      value,
+      className,
+      ...rest
+    }: RadioProps,
     ref,
   ): ReactElement => {
     const radioGroupContext = useContext(RadioGroupContext)
+    const enabled = useFeatureFlag('primer_react_css_modules_team')
     const handleOnChange: ChangeEventHandler = e => {
       radioGroupContext?.onChange && radioGroupContext.onChange(e)
       onChange && onChange(e)
@@ -110,6 +131,10 @@ const Radio = React.forwardRef(
         aria-invalid={validationStatus === 'error' ? 'true' : 'false'}
         sx={sxProp}
         onChange={handleOnChange}
+        className={clsx(className, {
+          [sharedClasses.Input]: enabled,
+          [classes.Radio]: enabled,
+        })}
         {...rest}
       />
     )

From 002be358676c2a882893dfbe518e1eafbd38db55 Mon Sep 17 00:00:00 2001
From: Jon Rohan 
Date: Wed, 30 Oct 2024 13:41:28 -0700
Subject: [PATCH 012/104] chore(Link): Remove the CSS modules feature flag from
 the Link component (#5148)

* Remove the CSS modules feature flag from Link

* Create sour-flies-camp.md

* Update snapshot

* Deprecate hoverColor and underline props
---
 .changeset/sour-flies-camp.md                 |   5 +
 packages/react/src/Link/Link.tsx              |  76 +--
 .../__snapshots__/Link.test.tsx.snap          | 180 +------
 .../__snapshots__/NavList.test.tsx.snap       | 476 +++++++-----------
 4 files changed, 200 insertions(+), 537 deletions(-)
 create mode 100644 .changeset/sour-flies-camp.md

diff --git a/.changeset/sour-flies-camp.md b/.changeset/sour-flies-camp.md
new file mode 100644
index 00000000000..11c1801e7a0
--- /dev/null
+++ b/.changeset/sour-flies-camp.md
@@ -0,0 +1,5 @@
+---
+"@primer/react": minor
+---
+
+Remove the CSS modules feature flag from the Link component
diff --git a/packages/react/src/Link/Link.tsx b/packages/react/src/Link/Link.tsx
index 608ca582edf..9c27320a776 100644
--- a/packages/react/src/Link/Link.tsx
+++ b/packages/react/src/Link/Link.tsx
@@ -1,18 +1,14 @@
 import {clsx} from 'clsx'
 import React, {forwardRef, useEffect} from 'react'
-import styled from 'styled-components'
-import {system} from 'styled-system'
-import {get} from '../constants'
 import {useRefObjectAsForwardedRef} from '../hooks'
 import type {SxProp} from '../sx'
-import sx from '../sx'
 import classes from './Link.module.css'
-import {useFeatureFlag} from '../FeatureFlags'
 import Box from '../Box'
 import type {ComponentProps} from '../utils/types'
 import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
 
 type StyledLinkProps = {
+  /** @deprecated use CSS modules to style hover color */
   hoverColor?: string
   muted?: boolean
   /** @deprecated use `inline` to specify the type of link instead */
@@ -21,49 +17,7 @@ type StyledLinkProps = {
   inline?: boolean
 } & SxProp
 
-const hoverColor = system({
-  hoverColor: {
-    property: 'color',
-    scale: 'colors',
-  },
-})
-
-const StyledLink = styled.a`
-  color: ${props => (props.muted ? get('colors.fg.muted')(props) : get('colors.accent.fg')(props))};
-
-  /* By default, Link does not have underline */
-  text-decoration: none;
-
-  /* You can add one by setting underline={true} */
-  text-decoration: ${props => (props.underline ? 'underline' : undefined)};
-
-  /* Inline links (inside a text block), however, should have underline based on accessibility setting set in data-attribute */
-  /* Note: setting underline={false} does not override this */
-  [data-a11y-link-underlines='true'] &[data-inline='true'] {
-    text-decoration: underline;
-  }
-
-  &:hover {
-    text-decoration: ${props => (props.muted ? 'none' : 'underline')};
-    ${props => (props.hoverColor ? hoverColor : props.muted ? `color: ${get('colors.accent.fg')(props)}` : '')};
-  }
-  &:is(button) {
-    display: inline-block;
-    padding: 0;
-    font-size: inherit;
-    white-space: nowrap;
-    cursor: pointer;
-    user-select: none;
-    background-color: transparent;
-    border: 0;
-    appearance: none;
-  }
-  ${sx};
-`
-
 const Link = forwardRef(({as: Component = 'a', className, inline, underline, ...props}, forwardedRef) => {
-  const enabled = useFeatureFlag('primer_react_css_modules_ga')
-
   const innerRef = React.useRef(null)
   useRefObjectAsForwardedRef(forwardedRef, innerRef)
 
@@ -91,24 +45,10 @@ const Link = forwardRef(({as: Component = 'a', className, inline, underline, ...
     }, [innerRef])
   }
 
-  if (enabled) {
-    if (props.sx) {
-      return (
-        
-      )
-    }
-
+  if (props.sx) {
     return (
-      
 `;
 
 exports[`Link passes href down to link element 1`] = `
-.c1 {
-  color: var(--fgColor-accent,var(--color-accent-fg,#0969da));
-  -webkit-text-decoration: none;
-  text-decoration: none;
-}
-
-[data-a11y-link-underlines='true'] .c0[data-inline='true'] {
-  -webkit-text-decoration: underline;
-  text-decoration: underline;
-}
-
-.c1:hover {
-  -webkit-text-decoration: underline;
-  text-decoration: underline;
-}
-
-.c1:is(button) {
-  display: inline-block;
-  padding: 0;
-  font-size: inherit;
-  white-space: nowrap;
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-  background-color: transparent;
-  border: 0;
-  -webkit-appearance: none;
-  -moz-appearance: none;
-  appearance: none;
-}
-
 
 `;
 
 exports[`Link respects hoverColor prop 1`] = `
-.c1 {
-  color: var(--fgColor-accent,var(--color-accent-fg,#0969da));
-  -webkit-text-decoration: none;
-  text-decoration: none;
-}
-
-[data-a11y-link-underlines='true'] .c0[data-inline='true'] {
-  -webkit-text-decoration: underline;
-  text-decoration: underline;
-}
-
-.c1:hover {
-  -webkit-text-decoration: underline;
-  text-decoration: underline;
-  color: var(--fgColor-accent,var(--color-accent-fg,#0969da));
-}
-
-.c1:is(button) {
-  display: inline-block;
-  padding: 0;
-  font-size: inherit;
-  white-space: nowrap;
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-  background-color: transparent;
-  border: 0;
-  -webkit-appearance: none;
-  -moz-appearance: none;
-  appearance: none;
-}
-
 
 `;
 
 exports[`Link respects the  "sx" prop when "muted" prop is also passed 1`] = `
-.c1 {
-  color: var(--fgColor-muted,var(--color-fg-muted,#656d76));
-  -webkit-text-decoration: none;
-  text-decoration: none;
+.c0 {
   color: var(--fgColor-onEmphasis,var(--color-fg-on-emphasis,#ffffff));
 }
 
-[data-a11y-link-underlines='true'] .c0[data-inline='true'] {
-  -webkit-text-decoration: underline;
-  text-decoration: underline;
-}
-
-.c1:hover {
-  -webkit-text-decoration: none;
-  text-decoration: none;
-  color: var(--fgColor-accent,var(--color-accent-fg,#0969da));
-}
-
-.c1:is(button) {
-  display: inline-block;
-  padding: 0;
-  font-size: inherit;
-  white-space: nowrap;
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-  background-color: transparent;
-  border: 0;
-  -webkit-appearance: none;
-  -moz-appearance: none;
-  appearance: none;
-}
-
 
 `;
 
 exports[`Link respects the "muted" prop 1`] = `
-.c1 {
-  color: var(--fgColor-muted,var(--color-fg-muted,#656d76));
-  -webkit-text-decoration: none;
-  text-decoration: none;
-}
-
-[data-a11y-link-underlines='true'] .c0[data-inline='true'] {
-  -webkit-text-decoration: underline;
-  text-decoration: underline;
-}
-
-.c1:hover {
-  -webkit-text-decoration: none;
-  text-decoration: none;
-  color: var(--fgColor-accent,var(--color-accent-fg,#0969da));
-}
-
-.c1:is(button) {
-  display: inline-block;
-  padding: 0;
-  font-size: inherit;
-  white-space: nowrap;
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-  background-color: transparent;
-  border: 0;
-  -webkit-appearance: none;
-  -moz-appearance: none;
-  appearance: none;
-}
-
 
 `;
diff --git a/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap b/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap
index c425395d008..1d4bccc1f5a 100644
--- a/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap
+++ b/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap
@@ -1,7 +1,30 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`NavList renders a simple list 1`] = `
-.c5 {
+.c3 {
+  padding-left: 8px;
+  padding-right: 8px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-flex: 1;
+  -webkit-flex-grow: 1;
+  -ms-flex-positive: 1;
+  flex-grow: 1;
+  border-radius: 6px;
+  color: inherit;
+}
+
+.c3:hover {
+  color: inherit;
+  -webkit-text-decoration: none;
+  text-decoration: none;
+}
+
+.c4 {
   display: -webkit-box;
   display: -webkit-flex;
   display: -ms-flexbox;
@@ -16,7 +39,7 @@ exports[`NavList renders a simple list 1`] = `
   min-width: 0;
 }
 
-.c6 {
+.c5 {
   -webkit-box-flex: 1;
   -webkit-flex-grow: 1;
   -ms-flex-positive: 1;
@@ -25,7 +48,7 @@ exports[`NavList renders a simple list 1`] = `
   word-break: break-word;
 }
 
-.c8 {
+.c7 {
   -webkit-box-flex: 1;
   -webkit-flex-grow: 1;
   -ms-flex-positive: 1;
@@ -151,7 +174,7 @@ exports[`NavList renders a simple list 1`] = `
   border-radius: 6px;
 }
 
-.c7 {
+.c6 {
   position: relative;
   display: -webkit-box;
   display: -webkit-flex;
@@ -183,27 +206,27 @@ exports[`NavList renders a simple list 1`] = `
   margin-bottom: unset;
 }
 
-.c7[data-loading] {
+.c6[data-loading] {
   cursor: default;
 }
 
-.c7[aria-disabled],
-.c7[data-inactive] {
+.c6[aria-disabled],
+.c6[data-inactive] {
   cursor: not-allowed;
 }
 
-.c7[aria-disabled] [data-component="ActionList.Checkbox"],
-.c7[data-inactive] [data-component="ActionList.Checkbox"] {
+.c6[aria-disabled] [data-component="ActionList.Checkbox"],
+.c6[data-inactive] [data-component="ActionList.Checkbox"] {
   cursor: not-allowed;
   background-color: var(--control-bgColor-disabled,rgba(175,184,193,0.2));
   border-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2));
 }
 
-.c7 [data-component="ActionList.Item--DividerContainer"] {
+.c6 [data-component="ActionList.Item--DividerContainer"] {
   position: relative;
 }
 
-.c7 [data-component="ActionList.Item--DividerContainer"]::before {
+.c6 [data-component="ActionList.Item--DividerContainer"]::before {
   content: " ";
   display: block;
   position: absolute;
@@ -214,7 +237,7 @@ exports[`NavList renders a simple list 1`] = `
   border-color: var(--divider-color,transparent);
 }
 
-.c7:not(:first-of-type) {
+.c6:not(:first-of-type) {
   --divider-color: var(--borderColor-muted,var(--color-action-list-item-inline-divider,rgba(208,215,222,0.48)));
 }
 
@@ -222,22 +245,22 @@ exports[`NavList renders a simple list 1`] = `
   --divider-color: transparent !important;
 }
 
-.c7:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]),
-.c7[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) {
+.c6:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]),
+.c6[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) {
   --divider-color: transparent;
 }
 
-.c7:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c1,
-.c7[data-focus-visible-added] + li {
+.c6:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c1,
+.c6[data-focus-visible-added] + li {
   --divider-color: transparent;
 }
 
-.c7[data-is-active-descendant] {
+.c6[data-is-active-descendant] {
   font-weight: 400;
   background-color: var(--control-transparent-bgColor-selected,var(--color-action-list-item-default-selected-bg,rgba(208,215,222,0.24)));
 }
 
-.c7[data-is-active-descendant]::after {
+.c6[data-is-active-descendant]::after {
   position: absolute;
   top: calc(50% - 12px);
   left: -8px;
@@ -248,59 +271,6 @@ exports[`NavList renders a simple list 1`] = `
   border-radius: 6px;
 }
 
-.c4 {
-  color: var(--fgColor-accent,var(--color-accent-fg,#0969da));
-  -webkit-text-decoration: none;
-  text-decoration: none;
-  padding-left: 8px;
-  padding-right: 8px;
-  padding-top: 6px;
-  padding-bottom: 6px;
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-box-flex: 1;
-  -webkit-flex-grow: 1;
-  -ms-flex-positive: 1;
-  flex-grow: 1;
-  border-radius: 6px;
-  color: inherit;
-}
-
-[data-a11y-link-underlines='true'] .c3[data-inline='true'] {
-  -webkit-text-decoration: underline;
-  text-decoration: underline;
-}
-
-.c4:hover {
-  -webkit-text-decoration: underline;
-  text-decoration: underline;
-}
-
-.c4:is(button) {
-  display: inline-block;
-  padding: 0;
-  font-size: inherit;
-  white-space: nowrap;
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  -ms-user-select: none;
-  user-select: none;
-  background-color: transparent;
-  border: 0;
-  -webkit-appearance: none;
-  -moz-appearance: none;
-  appearance: none;
-}
-
-.c4:hover {
-  color: inherit;
-  -webkit-text-decoration: none;
-  text-decoration: none;
-}
-
 @media (forced-colors:active) {
   .c2:focus,
   .c2:focus-visible,
@@ -332,30 +302,30 @@ exports[`NavList renders a simple list 1`] = `
 }
 
 @media (forced-colors:active) {
-  .c7:focus,
-  .c7:focus-visible,
-  .c7 > a.focus-visible,
-  .c7[data-is-active-descendant] {
+  .c6:focus,
+  .c6:focus-visible,
+  .c6 > a.focus-visible,
+  .c6[data-is-active-descendant] {
     outline: solid 1px transparent !important;
   }
 }
 
 @media (hover:hover) and (pointer:fine) {
-  .c7:hover:not([aria-disabled]):not([data-inactive]) {
+  .c6:hover:not([aria-disabled]):not([data-inactive]) {
     background-color: var(--control-transparent-bgColor-hover,var(--color-action-list-item-default-hover-bg,rgba(208,215,222,0.32)));
     color: var(--fgColor-default,var(--color-fg-default,#1F2328));
     box-shadow: inset 0 0 0 max(1px,0.0625rem) var(--control-transparent-borderColor-active,var(--color-action-list-item-default-active-border,transparent));
   }
 
-  .c7:focus-visible,
-  .c7 > a.focus-visible,
-  .c7:focus.focus-visible {
+  .c6:focus-visible,
+  .c6 > a.focus-visible,
+  .c6:focus.focus-visible {
     outline: none;
     border: 2 solid;
     box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da));
   }
 
-  .c7:active:not([aria-disabled]):not([data-inactive]) {
+  .c6:active:not([aria-disabled]):not([data-inactive]) {
     background-color: var(--control-transparent-bgColor-active,var(--color-action-list-item-default-active-bg,rgba(208,215,222,0.48)));
     color: var(--fgColor-default,var(--color-fg-default,#1F2328));
   }
@@ -374,17 +344,17 @@ exports[`NavList renders a simple list 1`] = `
         
           
Home @@ -393,21 +363,21 @@ exports[`NavList renders a simple list 1`] = `
  • Contact @@ -496,7 +466,30 @@ exports[`NavList renders with groups 1`] = ` padding-inline-start: 0; } -.c9 { +.c7 { + padding-left: 8px; + padding-right: 8px; + padding-top: 6px; + padding-bottom: 6px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + border-radius: 6px; + color: inherit; +} + +.c7:hover { + color: inherit; + -webkit-text-decoration: none; + text-decoration: none; +} + +.c8 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -511,7 +504,7 @@ exports[`NavList renders with groups 1`] = ` min-width: 0; } -.c10 { +.c9 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -520,7 +513,7 @@ exports[`NavList renders with groups 1`] = ` word-break: break-word; } -.c12 { +.c11 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -646,7 +639,7 @@ exports[`NavList renders with groups 1`] = ` border-radius: 6px; } -.c11 { +.c10 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -678,27 +671,27 @@ exports[`NavList renders with groups 1`] = ` margin-bottom: unset; } -.c11[data-loading] { +.c10[data-loading] { cursor: default; } -.c11[aria-disabled], -.c11[data-inactive] { +.c10[aria-disabled], +.c10[data-inactive] { cursor: not-allowed; } -.c11[aria-disabled] [data-component="ActionList.Checkbox"], -.c11[data-inactive] [data-component="ActionList.Checkbox"] { +.c10[aria-disabled] [data-component="ActionList.Checkbox"], +.c10[data-inactive] [data-component="ActionList.Checkbox"] { cursor: not-allowed; background-color: var(--control-bgColor-disabled,rgba(175,184,193,0.2)); border-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2)); } -.c11 [data-component="ActionList.Item--DividerContainer"] { +.c10 [data-component="ActionList.Item--DividerContainer"] { position: relative; } -.c11 [data-component="ActionList.Item--DividerContainer"]::before { +.c10 [data-component="ActionList.Item--DividerContainer"]::before { content: " "; display: block; position: absolute; @@ -709,7 +702,7 @@ exports[`NavList renders with groups 1`] = ` border-color: var(--divider-color,transparent); } -.c11:not(:first-of-type) { +.c10:not(:first-of-type) { --divider-color: var(--borderColor-muted,var(--color-action-list-item-inline-divider,rgba(208,215,222,0.48))); } @@ -717,22 +710,22 @@ exports[`NavList renders with groups 1`] = ` --divider-color: transparent !important; } -.c11:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), -.c11[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { +.c10:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), +.c10[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { --divider-color: transparent; } -.c11:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c5, -.c11[data-focus-visible-added] + li { +.c10:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c5, +.c10[data-focus-visible-added] + li { --divider-color: transparent; } -.c11[data-is-active-descendant] { +.c10[data-is-active-descendant] { font-weight: 400; background-color: var(--control-transparent-bgColor-selected,var(--color-action-list-item-default-selected-bg,rgba(208,215,222,0.24))); } -.c11[data-is-active-descendant]::after { +.c10[data-is-active-descendant]::after { position: absolute; top: calc(50% - 12px); left: -8px; @@ -743,59 +736,6 @@ exports[`NavList renders with groups 1`] = ` border-radius: 6px; } -.c8 { - color: var(--fgColor-accent,var(--color-accent-fg,#0969da)); - -webkit-text-decoration: none; - text-decoration: none; - padding-left: 8px; - padding-right: 8px; - padding-top: 6px; - padding-bottom: 6px; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; - border-radius: 6px; - color: inherit; -} - -[data-a11y-link-underlines='true'] .c7[data-inline='true'] { - -webkit-text-decoration: underline; - text-decoration: underline; -} - -.c8:hover { - -webkit-text-decoration: underline; - text-decoration: underline; -} - -.c8:is(button) { - display: inline-block; - padding: 0; - font-size: inherit; - white-space: nowrap; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-color: transparent; - border: 0; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.c8:hover { - color: inherit; - -webkit-text-decoration: none; - text-decoration: none; -} - @media (forced-colors:active) { .c6:focus, .c6:focus-visible, @@ -827,30 +767,30 @@ exports[`NavList renders with groups 1`] = ` } @media (forced-colors:active) { - .c11:focus, - .c11:focus-visible, - .c11 > a.focus-visible, - .c11[data-is-active-descendant] { + .c10:focus, + .c10:focus-visible, + .c10 > a.focus-visible, + .c10[data-is-active-descendant] { outline: solid 1px transparent !important; } } @media (hover:hover) and (pointer:fine) { - .c11:hover:not([aria-disabled]):not([data-inactive]) { + .c10:hover:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-hover,var(--color-action-list-item-default-hover-bg,rgba(208,215,222,0.32))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); box-shadow: inset 0 0 0 max(1px,0.0625rem) var(--control-transparent-borderColor-active,var(--color-action-list-item-default-active-border,transparent)); } - .c11:focus-visible, - .c11 > a.focus-visible, - .c11:focus.focus-visible { + .c10:focus-visible, + .c10 > a.focus-visible, + .c10:focus.focus-visible { outline: none; border: 2 solid; box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da)); } - .c11:active:not([aria-disabled]):not([data-inactive]) { + .c10:active:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-active,var(--color-action-list-item-default-active-bg,rgba(208,215,222,0.48))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } @@ -892,17 +832,17 @@ exports[`NavList renders with groups 1`] = `
    Getting started @@ -936,21 +876,21 @@ exports[`NavList renders with groups 1`] = ` class="c4" >
  • Avatar @@ -1020,7 +960,32 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } -.c14 { +.c12 { + padding-left: 16px; + padding-right: 8px; + padding-top: 6px; + padding-bottom: 6px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + border-radius: 6px; + color: inherit; + font-size: 12px; + font-weight: 400; +} + +.c12:hover { + color: inherit; + -webkit-text-decoration: none; + text-decoration: none; +} + +.c13 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -1046,8 +1011,8 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav --divider-color: transparent !important; } -.c15:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c3, -.c15[data-focus-visible-added] + li { +.c14:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c3, +.c14[data-focus-visible-added] + li { --divider-color: transparent; } @@ -1258,61 +1223,6 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav border-radius: 6px; } -.c13 { - color: var(--fgColor-accent,var(--color-accent-fg,#0969da)); - -webkit-text-decoration: none; - text-decoration: none; - padding-left: 16px; - padding-right: 8px; - padding-top: 6px; - padding-bottom: 6px; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; - border-radius: 6px; - color: inherit; - font-size: 12px; - font-weight: 400; -} - -[data-a11y-link-underlines='true'] .c12[data-inline='true'] { - -webkit-text-decoration: underline; - text-decoration: underline; -} - -.c13:hover { - -webkit-text-decoration: underline; - text-decoration: underline; -} - -.c13:is(button) { - display: inline-block; - padding: 0; - font-size: inherit; - white-space: nowrap; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-color: transparent; - border: 0; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.c13:hover { - color: inherit; - -webkit-text-decoration: none; - text-decoration: none; -} - .c9 { -webkit-transform: rotate(180deg); -ms-transform: rotate(180deg); @@ -1453,7 +1363,7 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav Sub Item @@ -1531,6 +1441,31 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t display: none; } +.c12 { + padding-left: 16px; + padding-right: 8px; + padding-top: 6px; + padding-bottom: 6px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + border-radius: 6px; + color: inherit; + font-size: 12px; + font-weight: 400; +} + +.c12:hover { + color: inherit; + -webkit-text-decoration: none; + text-decoration: none; +} + .c7 { -webkit-box-flex: 1; -webkit-flex-grow: 1; @@ -1551,8 +1486,8 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t --divider-color: transparent !important; } -.c14:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c3, -.c14[data-focus-visible-added] + li { +.c13:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c3, +.c13[data-focus-visible-added] + li { --divider-color: transparent; } @@ -1670,8 +1605,8 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t --divider-color: transparent !important; } -.c15:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c3, -.c15[data-focus-visible-added] + li { +.c14:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c3, +.c14[data-focus-visible-added] + li { --divider-color: transparent; } @@ -1784,61 +1719,6 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t border-radius: 6px; } -.c13 { - color: var(--fgColor-accent,var(--color-accent-fg,#0969da)); - -webkit-text-decoration: none; - text-decoration: none; - padding-left: 16px; - padding-right: 8px; - padding-top: 6px; - padding-bottom: 6px; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; - border-radius: 6px; - color: inherit; - font-size: 12px; - font-weight: 400; -} - -[data-a11y-link-underlines='true'] .c12[data-inline='true'] { - -webkit-text-decoration: underline; - text-decoration: underline; -} - -.c13:hover { - -webkit-text-decoration: underline; - text-decoration: underline; -} - -.c13:is(button) { - display: inline-block; - padding: 0; - font-size: inherit; - white-space: nowrap; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-color: transparent; - border: 0; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.c13:hover { - color: inherit; - -webkit-text-decoration: none; - text-decoration: none; -} - .c9 { -webkit-transform: rotate(0deg); -ms-transform: rotate(0deg); @@ -1987,7 +1867,7 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t Date: Wed, 30 Oct 2024 18:16:02 -0400 Subject: [PATCH 013/104] feat(Header): Convert Header to CSS modules behind team feature flag (#5192) * feat(Header): Convert Header to CSS modules behind team feature flag * dev stories * prettier fix * update types * Fix missing 'as' prop and update tests * format * replace full class with data attr --- .changeset/odd-frogs-listen.md | 5 + .../react/src/Header/Header.dev.module.css | 11 ++ .../react/src/Header/Header.dev.stories.tsx | 76 ++++++++ packages/react/src/Header/Header.module.css | 39 ++++ packages/react/src/Header/Header.tsx | 178 ++++++++++++------ packages/react/src/__tests__/Header.test.tsx | 62 ++++++ .../internal/utils/toggleStyledComponent.tsx | 2 +- 7 files changed, 315 insertions(+), 58 deletions(-) create mode 100644 .changeset/odd-frogs-listen.md create mode 100644 packages/react/src/Header/Header.dev.module.css create mode 100644 packages/react/src/Header/Header.dev.stories.tsx create mode 100644 packages/react/src/Header/Header.module.css diff --git a/.changeset/odd-frogs-listen.md b/.changeset/odd-frogs-listen.md new file mode 100644 index 00000000000..9e89ad0a991 --- /dev/null +++ b/.changeset/odd-frogs-listen.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Update `Header` component to use CSS modules behind the feature flag primer_react_css_modules_team diff --git a/packages/react/src/Header/Header.dev.module.css b/packages/react/src/Header/Header.dev.module.css new file mode 100644 index 00000000000..8d90de7725a --- /dev/null +++ b/packages/react/src/Header/Header.dev.module.css @@ -0,0 +1,11 @@ +.HeaderDev { + background-color: var(--label-olive-bgColor-active); +} + +.HeaderDevItem { + padding-left: var(--base-size-24); +} + +.HeaderDevLink { + color: var(--color-prettylights-syntax-markup-inserted-text); +} diff --git a/packages/react/src/Header/Header.dev.stories.tsx b/packages/react/src/Header/Header.dev.stories.tsx new file mode 100644 index 00000000000..66756f36e93 --- /dev/null +++ b/packages/react/src/Header/Header.dev.stories.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import type {Meta} from '@storybook/react' +import {MarkGithubIcon} from '@primer/octicons-react' + +import Header from './Header' +import Avatar from '../Avatar' +import Octicon from '../Octicon' + +import classes from './Header.dev.module.css' +import {FeatureFlags} from '../FeatureFlags' + +export default { + title: 'Components/Header/Dev', + component: Header, +} as Meta + +export const WithCss = () => ( + +
    + + + + GitHub + + + Menu + + + +
    +
    +) + +export const WithSx = () => ( +
    + + + + GitHub + + + Menu + + + +
    +) + +export const WithSxAndCSS = () => ( + +
    + + + + GitHub + + + Menu + + + +
    +
    +) diff --git a/packages/react/src/Header/Header.module.css b/packages/react/src/Header/Header.module.css new file mode 100644 index 00000000000..0d8a055fea9 --- /dev/null +++ b/packages/react/src/Header/Header.module.css @@ -0,0 +1,39 @@ +.Header { + z-index: 32; + display: flex; + padding: var(--base-size-16); + overflow: auto; + font-size: var(--text-body-size-medium); + line-height: var(--text-title-lineHeight-large); + color: var(--header-fgColor-default); + background-color: var(--header-bgColor); + align-items: center; + flex-wrap: nowrap; +} + +.HeaderItem { + display: flex; + margin-right: var(--base-size-16); + align-self: stretch; + align-items: center; + flex-wrap: nowrap; + + &:where([data-full]) { + flex: auto; + } +} + +.HeaderLink { + display: flex; + font-weight: var(--text-title-weight-large); + color: var(--header-fgColor-logo); + text-decoration: none; + white-space: nowrap; + cursor: pointer; + align-items: center; + + &:hover, + &:focus { + color: var(--header-fgColor-default); + } +} diff --git a/packages/react/src/Header/Header.tsx b/packages/react/src/Header/Header.tsx index c676ccd2d4c..3c7c194ca43 100644 --- a/packages/react/src/Header/Header.tsx +++ b/packages/react/src/Header/Header.tsx @@ -4,68 +4,132 @@ import {get} from '../constants' import type {SxProp} from '../sx' import sx from '../sx' import type {ComponentProps} from '../utils/types' +import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent' +import {useFeatureFlag} from '../FeatureFlags' +import React from 'react' +import {clsx} from 'clsx' +import classes from './Header.module.css' +import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' -type StyledHeaderItemProps = {full?: boolean} & SxProp -type StyledHeaderProps = SxProp -type StyledHeaderLinkProps = {to?: Location | Pathname} & SxProp - -const Header = styled.header` - z-index: 32; - display: flex; - padding: ${get('space.3')}; - font-size: ${get('fontSizes.1')}; - line-height: ${get('lineHeights.default')}; - color: ${get('colors.header.text')}; - background-color: ${get('colors.header.bg')}; - align-items: center; - flex-wrap: nowrap; - overflow: auto; - - ${sx}; -` -const HeaderItem = styled.div` - display: flex; - margin-right: ${get('space.3')}; - align-self: stretch; - align-items: center; - flex-wrap: nowrap; - - ${({full}) => - full && - css` - flex: auto; - `}; - - ${sx}; -` +type StyledHeaderProps = React.ComponentProps<'header'> & SxProp +type StyledHeaderItemProps = React.ComponentProps<'div'> & SxProp & {full?: boolean} +type StyledHeaderLinkProps = React.ComponentProps<'a'> & SxProp & {to?: Location | Pathname} -HeaderItem.displayName = 'Header.Item' +const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team' -const HeaderLink = styled.a.attrs(({to}) => { - const isReactRouter = typeof to === 'string' - if (isReactRouter) { - // according to their docs, NavLink supports aria-current: - // https://reacttraining.com/react-router/web/api/NavLink/aria-current-string - return {'aria-current': 'page'} - } else { - return {} - } -})` - font-weight: ${get('fontWeights.bold')}; - color: ${get('colors.header.logo')}; - white-space: nowrap; - cursor: pointer; - text-decoration: none; - display: flex; - align-items: center; - - &:hover, - &:focus { +const StyledHeader = toggleStyledComponent( + CSS_MODULES_FEATURE_FLAG, + 'header', + styled.header` + z-index: 32; + display: flex; + padding: ${get('space.3')}; + font-size: ${get('fontSizes.1')}; + line-height: ${get('lineHeights.default')}; color: ${get('colors.header.text')}; - } + background-color: ${get('colors.header.bg')}; + align-items: center; + flex-wrap: nowrap; + overflow: auto; + + ${sx}; + `, +) + +const Header = React.forwardRef(function Header( + {children, className, ...rest}, + forwardRef, +) { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + return ( + + {children} + + ) +}) as PolymorphicForwardRefComponent<'header', StyledHeaderProps> + +Header.displayName = 'Header' + +const StyledHeaderItem = toggleStyledComponent( + CSS_MODULES_FEATURE_FLAG, + 'div', + styled.div` + display: flex; + margin-right: ${get('space.3')}; + align-self: stretch; + align-items: center; + flex-wrap: nowrap; + + ${({full}) => + full && + css` + flex: auto; + `}; + + ${sx}; + `, +) + +const HeaderItem = React.forwardRef(function HeaderItem( + {children, className, ...rest}, + forwardRef, +) { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + return ( + + {children} + + ) +}) + +HeaderItem.displayName = 'Header.Item' + +const StyledHeaderLink = toggleStyledComponent( + CSS_MODULES_FEATURE_FLAG, + 'a', + styled.a.attrs(({to}) => { + const isReactRouter = typeof to === 'string' + if (isReactRouter) { + // according to their docs, NavLink supports aria-current: + // https://reacttraining.com/react-router/web/api/NavLink/aria-current-string + return {'aria-current': 'page'} + } else { + return {} + } + })` + font-weight: ${get('fontWeights.bold')}; + color: ${get('colors.header.logo')}; + white-space: nowrap; + cursor: pointer; + text-decoration: none; + display: flex; + align-items: center; + + &:hover, + &:focus { + color: ${get('colors.header.text')}; + } + + ${sx}; + `, +) - ${sx}; -` +const HeaderLink = React.forwardRef(function HeaderLink( + {children, className, ...rest}, + forwardRef, +) { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + return ( + + {children} + + ) +}) HeaderLink.displayName = 'Header.Link' diff --git a/packages/react/src/__tests__/Header.test.tsx b/packages/react/src/__tests__/Header.test.tsx index c4af28bc8ac..e084cc9e8c6 100644 --- a/packages/react/src/__tests__/Header.test.tsx +++ b/packages/react/src/__tests__/Header.test.tsx @@ -3,6 +3,7 @@ import {Header} from '..' import {render, behavesAsComponent, checkExports} from '../utils/testing' import {render as HTMLRender} from '@testing-library/react' import axe from 'axe-core' +import {FeatureFlags} from '../FeatureFlags' describe('Header', () => { behavesAsComponent({Component: Header}) @@ -13,10 +14,52 @@ describe('Header', () => { describe('Header.Item', () => { behavesAsComponent({Component: Header.Item}) + + it('accepts and applies className', () => { + expect(render().props.className).toContain('primer') + }) + + it('should support `className` on the outermost element', () => { + const Element = () => + const FeatureFlagElement = () => { + return ( + + + + ) + } + expect(HTMLRender().container.firstChild).toHaveClass('test-class-name') + expect(HTMLRender().container.firstChild).toHaveClass('test-class-name') + }) }) describe('Header.Link', () => { behavesAsComponent({Component: Header.Link}) + + it('should support `className` on the outermost element', () => { + const Element = () => + const FeatureFlagElement = () => { + return ( + + + + ) + } + expect(HTMLRender().container.firstChild).toHaveClass('test-class-name') + expect(HTMLRender().container.firstChild).toHaveClass('test-class-name') + }) }) it('should have no axe violations', async () => { @@ -43,4 +86,23 @@ describe('Header', () => { it('sets aria-label appropriately', () => { expect(render(
    ).props['aria-label']).toEqual('Test label') }) + + it('should support `className` on the outermost element', () => { + const Element = () =>
    + const FeatureFlagElement = () => { + return ( + + + + ) + } + expect(HTMLRender().container.firstChild).toHaveClass('test-class-name') + expect(HTMLRender().container.firstChild).toHaveClass('test-class-name') + }) }) diff --git a/packages/react/src/internal/utils/toggleStyledComponent.tsx b/packages/react/src/internal/utils/toggleStyledComponent.tsx index 15d4932d5c7..96f7eae70ba 100644 --- a/packages/react/src/internal/utils/toggleStyledComponent.tsx +++ b/packages/react/src/internal/utils/toggleStyledComponent.tsx @@ -17,7 +17,7 @@ type CSSModulesProps = { * * @param flag - the feature flag that will control whether or not the provided * styled component is used - * @param defautlAs - the default component to use when `as` is not provided + * @param defaultAs - the default component to use when `as` is not provided * @param Component - the styled component that will be used if the feature flag * is disabled */ From 8138dee8d8235475ec8135591c52abf3df90d50b Mon Sep 17 00:00:00 2001 From: Alon Dahari Date: Thu, 31 Oct 2024 11:26:21 +0000 Subject: [PATCH 014/104] Catch tooltip v2 errors in older browsers (#5171) * Catch errors in tooltip functions * Update changeset --- .changeset/grumpy-lamps-behave.md | 5 ++ packages/react/src/TooltipV2/Tooltip.tsx | 92 ++++++++++++++++-------- 2 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 .changeset/grumpy-lamps-behave.md diff --git a/.changeset/grumpy-lamps-behave.md b/.changeset/grumpy-lamps-behave.md new file mode 100644 index 00000000000..9ee089cb00e --- /dev/null +++ b/.changeset/grumpy-lamps-behave.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +catch TooltipV2 errors in old browsers as a temp fix for unnecessary Sentry reports diff --git a/packages/react/src/TooltipV2/Tooltip.tsx b/packages/react/src/TooltipV2/Tooltip.tsx index 4c574664b31..7a1eebf66a3 100644 --- a/packages/react/src/TooltipV2/Tooltip.tsx +++ b/packages/react/src/TooltipV2/Tooltip.tsx @@ -198,40 +198,72 @@ export const Tooltip = React.forwardRef( const [isPopoverOpen, setIsPopoverOpen] = useState(false) const openTooltip = () => { - if ( - tooltipElRef.current && - triggerRef.current && - tooltipElRef.current.hasAttribute('popover') && - !tooltipElRef.current.matches(':popover-open') - ) { - const tooltip = tooltipElRef.current - const trigger = triggerRef.current - tooltip.showPopover() - setIsPopoverOpen(true) - /* - * TOOLTIP POSITIONING - */ - const settings = { - side: directionToPosition[direction].side, - align: directionToPosition[direction].align, + try { + if ( + tooltipElRef.current && + triggerRef.current && + tooltipElRef.current.hasAttribute('popover') && + !tooltipElRef.current.matches(':popover-open') + ) { + const tooltip = tooltipElRef.current + const trigger = triggerRef.current + tooltip.showPopover() + setIsPopoverOpen(true) + /* + * TOOLTIP POSITIONING + */ + const settings = { + side: directionToPosition[direction].side, + align: directionToPosition[direction].align, + } + const {top, left, anchorAlign, anchorSide} = getAnchoredPosition(tooltip, trigger, settings) + // This is required to make sure the popover is positioned correctly i.e. when there is not enough space on the specified direction, we set a new direction to position the ::after + const calculatedDirection = positionToDirection[`${anchorSide}-${anchorAlign}` as string] + setCalculatedDirection(calculatedDirection) + tooltip.style.top = `${top}px` + tooltip.style.left = `${left}px` + } + } catch (error) { + // older browsers don't support the :popover-open selector and will throw, even though we use a polyfill + // see https://github.com/github/issues/issues/12468 + if ( + error && + typeof error === 'object' && + 'message' in error && + typeof error.message === 'string' && + error.message.includes('not a valid selector') + ) { + // fail silently + } else { + throw error } - const {top, left, anchorAlign, anchorSide} = getAnchoredPosition(tooltip, trigger, settings) - // This is required to make sure the popover is positioned correctly i.e. when there is not enough space on the specified direction, we set a new direction to position the ::after - const calculatedDirection = positionToDirection[`${anchorSide}-${anchorAlign}` as string] - setCalculatedDirection(calculatedDirection) - tooltip.style.top = `${top}px` - tooltip.style.left = `${left}px` } } const closeTooltip = () => { - if ( - tooltipElRef.current && - triggerRef.current && - tooltipElRef.current.hasAttribute('popover') && - tooltipElRef.current.matches(':popover-open') - ) { - tooltipElRef.current.hidePopover() - setIsPopoverOpen(false) + try { + if ( + tooltipElRef.current && + triggerRef.current && + tooltipElRef.current.hasAttribute('popover') && + tooltipElRef.current.matches(':popover-open') + ) { + tooltipElRef.current.hidePopover() + setIsPopoverOpen(false) + } + } catch (error) { + // older browsers don't support the :popover-open selector and will throw, even though we use a polyfill + // see https://github.com/github/issues/issues/12468 + if ( + error && + typeof error === 'object' && + 'message' in error && + typeof error.message === 'string' && + error.message.includes('not a valid selector') + ) { + // fail silently + } else { + throw error + } } } From 719def7ea83f5212dc6ba888fe22d29b6620ce37 Mon Sep 17 00:00:00 2001 From: Randall Krauskopf <104226843+randall-krauskopf@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:57:25 -0500 Subject: [PATCH 015/104] feat(Select): Convert `Select` component to CSS Modules behind feature flag (#5194) * initial commit * changeset and lint * fix css module comments * fix Select import to please the FormControl component's type comparision * fix slot comparison in form control --- .changeset/modern-icons-clean.md | 5 + .../react/src/FormControl/FormControl.tsx | 2 +- .../react/src/Select/Select.dev.stories.tsx | 25 +++ .../src/Select/Select.features.stories.tsx | 3 +- packages/react/src/Select/Select.figma.tsx | 2 +- packages/react/src/Select/Select.module.css | 67 ++++++ packages/react/src/Select/Select.stories.tsx | 5 +- packages/react/src/Select/Select.tsx | 199 +++++++++++------- packages/react/src/index.ts | 5 +- 9 files changed, 235 insertions(+), 78 deletions(-) create mode 100644 .changeset/modern-icons-clean.md create mode 100644 packages/react/src/Select/Select.dev.stories.tsx create mode 100644 packages/react/src/Select/Select.module.css diff --git a/.changeset/modern-icons-clean.md b/.changeset/modern-icons-clean.md new file mode 100644 index 00000000000..ac697326b5e --- /dev/null +++ b/.changeset/modern-icons-clean.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Migrate `Select` component to css modules diff --git a/packages/react/src/FormControl/FormControl.tsx b/packages/react/src/FormControl/FormControl.tsx index a6ccd15c858..f2eb061b594 100644 --- a/packages/react/src/FormControl/FormControl.tsx +++ b/packages/react/src/FormControl/FormControl.tsx @@ -3,7 +3,7 @@ import Autocomplete from '../Autocomplete' import Box from '../Box' import Checkbox from '../Checkbox' import Radio from '../Radio' -import Select from '../Select' +import Select from '../Select/Select' import {SelectPanel} from '../SelectPanel' import TextInput from '../TextInput' import TextInputWithTokens from '../TextInputWithTokens' diff --git a/packages/react/src/Select/Select.dev.stories.tsx b/packages/react/src/Select/Select.dev.stories.tsx new file mode 100644 index 00000000000..29faba3b0bc --- /dev/null +++ b/packages/react/src/Select/Select.dev.stories.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import type {Meta} from '@storybook/react' +import {FormControl, Box} from '..' +import Select from './Select' + +export default { + title: 'Components/Select/Dev', + component: Select, +} as Meta + +export const Default = () => ( + + + Default label + + + +) diff --git a/packages/react/src/Select/Select.features.stories.tsx b/packages/react/src/Select/Select.features.stories.tsx index 1279d3d0dd6..7a1fff357bb 100644 --- a/packages/react/src/Select/Select.features.stories.tsx +++ b/packages/react/src/Select/Select.features.stories.tsx @@ -1,5 +1,6 @@ import React from 'react' -import {Select, FormControl, Box, Heading} from '..' +import {FormControl, Box, Heading} from '..' +import Select from './Select' export default { title: 'Components/Select/Features', diff --git a/packages/react/src/Select/Select.figma.tsx b/packages/react/src/Select/Select.figma.tsx index 7c75ad3f8e6..a4cbbef3fd9 100644 --- a/packages/react/src/Select/Select.figma.tsx +++ b/packages/react/src/Select/Select.figma.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Select} from '../../src' +import Select from '.' import FormControl from '../FormControl' import figma from '@figma/code-connect' diff --git a/packages/react/src/Select/Select.module.css b/packages/react/src/Select/Select.module.css new file mode 100644 index 00000000000..f3175cd0967 --- /dev/null +++ b/packages/react/src/Select/Select.module.css @@ -0,0 +1,67 @@ +.Select { + width: 100%; + /* stylelint-disable-next-line primer/spacing */ + margin-top: 1px; + /* stylelint-disable-next-line primer/spacing */ + margin-bottom: 1px; + /* stylelint-disable-next-line primer/spacing */ + margin-left: 1px; + font-size: inherit; + color: currentColor; + + /* Firefox hacks: + * 1. Makes Firefox's native dropdown menu's background match the theme. + * background-color should be 'transparent', but Firefox uses the background-color on + * so the background color doesn't hide the focus outline created with an inset box-shadow. + */ + background-color: inherit; + border: 0; + border-radius: inherit; + outline: none; + appearance: none; + + /* 2. Prevents visible overlap of partially transparent background colors. + * 'colors.input.disabledBg' happens to be partially transparent in light mode, so we use a + * transparent background-color on a disabled 's background color white when setting 'background-color: transparent;' + */ + @media screen and (forced-colors: active) { + &:disabled { + background-color: -moz-combobox; + } + } +} + +.TextInputWrapper { + position: relative; + overflow: hidden; + + @media screen and (forced-colors: active) { + svg { + fill: 'FieldText'; + } + } +} + +.disabled { + @media screen and (forced-colors: active) { + svg { + fill: 'GrayText'; + } + } +} + +.ArrowIndicator { + position: absolute; + top: 50%; + right: var(--base-size-4); + pointer-events: none; + transform: translateY(-50%); +} diff --git a/packages/react/src/Select/Select.stories.tsx b/packages/react/src/Select/Select.stories.tsx index 2cb9aeb0fb4..042f85c7d4f 100644 --- a/packages/react/src/Select/Select.stories.tsx +++ b/packages/react/src/Select/Select.stories.tsx @@ -1,7 +1,8 @@ import React from 'react' import type {Meta} from '@storybook/react' -import {Select, FormControl, Box} from '..' -import type {SelectProps} from '../Select' +import {FormControl, Box} from '..' +import Select from './Select' +import type {SelectProps} from './Select' import type {FormControlArgs} from '../utils/form-story-helpers' import { formControlArgs, diff --git a/packages/react/src/Select/Select.tsx b/packages/react/src/Select/Select.tsx index ee8eb099e22..36152a035b7 100644 --- a/packages/react/src/Select/Select.tsx +++ b/packages/react/src/Select/Select.tsx @@ -1,7 +1,11 @@ import React from 'react' import styled from 'styled-components' +import {clsx} from 'clsx' import type {StyledWrapperProps} from '../internal/components/TextInputWrapper' import TextInputWrapper from '../internal/components/TextInputWrapper' +import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './Select.module.css' export type SelectProps = Omit< Omit, 'size'> & Omit, @@ -10,62 +14,70 @@ export type SelectProps = Omit< placeholder?: string } -const arrowRightOffset = '4px' +const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team' -const StyledSelect = styled.select` - appearance: none; - border-radius: inherit; - border: 0; - color: currentColor; - font-size: inherit; - outline: none; - width: 100%; +const arrowRightOffset = '4px' - /* Firefox hacks: */ - /* 1. Makes Firefox's native dropdown menu's background match the theme. +const StyledSelect = toggleStyledComponent( + CSS_MODULES_FEATURE_FLAG, + 'select', + styled.select` + appearance: none; + border-radius: inherit; + border: 0; + color: currentColor; + font-size: inherit; + outline: none; + width: 100%; + + /* Firefox hacks: */ + /* 1. Makes Firefox's native dropdown menu's background match the theme. background-color should be 'transparent', but Firefox uses the background-color on so the background color doesn't hide the focus outline created with an inset box-shadow. */ - background-color: inherit; - margin-top: 1px; - margin-left: 1px; - margin-bottom: 1px; + background-color: inherit; + margin-top: 1px; + margin-left: 1px; + margin-bottom: 1px; - /* 2. Prevents visible overlap of partially transparent background colors. + /* 2. Prevents visible overlap of partially transparent background colors. 'colors.input.disabledBg' happens to be partially transparent in light mode, so we use a transparent background-color on a disabled 's background color white when setting 'background-color: transparent;' */ - @media screen and (forced-colors: active) { - &:disabled { - background-color: -moz-combobox; + @media screen and (forced-colors: active) { + &:disabled { + background-color: -moz-combobox; + } } - } -` - -const ArrowIndicatorSVG: React.FC> = ({className}) => ( - + `, ) -const ArrowIndicator = styled(ArrowIndicatorSVG)` +const ArrowIndicatorSVG: React.FC> = ({className}) => { + return ( + + ) +} + +const StyledArrowIndicatorSVG = styled(ArrowIndicatorSVG)` pointer-events: none; position: absolute; right: ${arrowRightOffset}; @@ -73,43 +85,88 @@ const ArrowIndicator = styled(ArrowIndicatorSVG)` transform: translateY(-50%); ` +const ArrowIndicator: React.FC<{className?: string}> = ({className}) => { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + if (enabled) { + return + } + + return +} + const Select = React.forwardRef( - ({block, children, contrast, disabled, placeholder, size, required, validationStatus, ...rest}: SelectProps, ref) => ( - { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + if (enabled) { + return ( + + + {placeholder && ( + + )} + {children} + + + + ) + } + + return ( + - - {placeholder && ( - - )} - {children} - - - - ), + + {placeholder && ( + + )} + {children} + + + + ) + }, ) const Option: React.FC & {value: string}>> = props => ( diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index a6188c13cee..b5f7992b67c 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -137,8 +137,9 @@ export {default as RadioGroup} from './RadioGroup' export type {RelativeTimeProps} from './RelativeTime' export {default as RelativeTime} from './RelativeTime' export {SegmentedControl} from './SegmentedControl' -export {default as Select} from './Select' -export type {SelectProps} from './Select' +// Curently there is a duplicate Select component at the root of the dir, so need to be explicit about exporting from the src/Select dir +export {default as Select} from './Select/Select' +export type {SelectProps} from './Select/Select' export {SelectPanel} from './SelectPanel' export type {SelectPanelProps} from './SelectPanel' export {default as SideNav} from './SideNav' From 8d9a357db49dbf1f00e19c7aa489bd963a0d3dd5 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Thu, 31 Oct 2024 13:10:13 -0500 Subject: [PATCH 016/104] refactor(BranchName): update BranchName to use CSS Modules (#5040) * refactor(BranchName): update BranchName to use CSS Modules * chore: add changeset * fix: use sx instead of defaultSxProp * chore: remove defaultSxProp * chore: experiment with forward ref * Remove feature flag duplication * Add test for `className` support in `BranchName` * refactor: update usage for forwardRef * chore: update fallthrough to use as prop * Update BranchName.displayname * test: add option to skip display name check --------- Co-authored-by: Josh Black Co-authored-by: Jon Rohan --- .changeset/four-schools-grin.md | 5 + e2e/components/BranchName.test.ts | 142 ++++++------------ .../src/BranchName/BranchName.module.css | 14 ++ packages/react/src/BranchName/BranchName.tsx | 57 ++++++- .../BranchName/__tests__/BranchName.test.tsx | 27 +++- .../__tests__/BranchName.types.test.tsx | 40 +++++ packages/react/src/utils/testing.tsx | 9 +- 7 files changed, 189 insertions(+), 105 deletions(-) create mode 100644 .changeset/four-schools-grin.md create mode 100644 packages/react/src/BranchName/BranchName.module.css diff --git a/.changeset/four-schools-grin.md b/.changeset/four-schools-grin.md new file mode 100644 index 00000000000..72c447d1a85 --- /dev/null +++ b/.changeset/four-schools-grin.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Update BranchName to use CSS Modules behind feature flag diff --git a/e2e/components/BranchName.test.ts b/e2e/components/BranchName.test.ts index feb776b3101..a735fa8ae31 100644 --- a/e2e/components/BranchName.test.ts +++ b/e2e/components/BranchName.test.ts @@ -2,110 +2,58 @@ import {test, expect} from '@playwright/test' import {visit} from '../test-helpers/storybook' import {themes} from '../test-helpers/themes' -test.describe('BranchName', () => { - test.describe('Default', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-branchname--default', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`BranchName.Default.${theme}.png`) - - // Focus state - await page.keyboard.press('Tab') - expect(await page.screenshot()).toMatchSnapshot(`BranchName.Default.${theme}.focus.png`) - }) +const stories = [ + { + title: 'Default', + id: 'components-branchname--default', + focus: true, + }, + { + title: 'Not A Link', + id: 'components-branchname-features--not-a-link', + focus: false, + }, + { + title: 'With A Branch Icon', + id: 'components-branchname-features--with-branch-icon', + focus: false, + }, +] as const - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-branchname--default', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', +test.describe('BranchName', () => { + for (const story of stories) { + test.describe(story.title, () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, }, - }, - }) - }) - }) - } - }) - - test.describe('Not A Link', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-branchname-features--not-a-link', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`BranchName.Not A Link.${theme}.png`) - }) + }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-branchname-features--not-a-link', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) - }) - } - }) + // Default state + expect(await page.screenshot()).toMatchSnapshot(`BranchName.${story.title}.${theme}.png`) - test.describe('With A Branch Icon', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-branchname-features--with-branch-icon', - globals: { - colorScheme: theme, - }, + // Focus state + if (story.focus) { + await page.keyboard.press('Tab') + expect(await page.screenshot()).toMatchSnapshot(`BranchName.${story.title}.${theme}.focus.png`) + } }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`BranchName.With A Branch Icon.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-branchname-features--with-branch-icon', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', + test('axe @aat', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, }, - }, + }) + await expect(page).toHaveNoViolations() }) }) - }) - } - }) + } + }) + } }) diff --git a/packages/react/src/BranchName/BranchName.module.css b/packages/react/src/BranchName/BranchName.module.css new file mode 100644 index 00000000000..66e9abc2d50 --- /dev/null +++ b/packages/react/src/BranchName/BranchName.module.css @@ -0,0 +1,14 @@ +.BranchName { + display: inline-block; + padding: var(--base-size-2) var(--base-size-6); + font-family: var(--fontStack-monospace); + font-size: var(--text-body-size-small); + color: var(--fgColor-link); + text-decoration: none; + background-color: var(--bgColor-accent-muted); + border-radius: var(--borderRadius-medium); + + &:is(:not(a)) { + color: var(--fgColor-muted); + } +} diff --git a/packages/react/src/BranchName/BranchName.tsx b/packages/react/src/BranchName/BranchName.tsx index 010777c4a2a..4b6d7697c9f 100644 --- a/packages/react/src/BranchName/BranchName.tsx +++ b/packages/react/src/BranchName/BranchName.tsx @@ -1,10 +1,14 @@ +import React, {type ForwardedRef} from 'react' +import {clsx} from 'clsx' import styled from 'styled-components' import {get} from '../constants' import type {SxProp} from '../sx' import sx from '../sx' -import type {ComponentProps} from '../utils/types' +import {useFeatureFlag} from '../FeatureFlags' +import Box from '../Box' +import classes from './BranchName.module.css' -const BranchName = styled.a` +const StyledBranchName = styled.a` display: inline-block; padding: 2px 6px; font-size: var(--text-body-size-small, ${get('fontSizes.0')}); @@ -19,5 +23,50 @@ const BranchName = styled.a` ${sx}; ` -export type BranchNameProps = ComponentProps -export default BranchName +type BranchNameProps = { + as?: As +} & DistributiveOmit, 'as'> & + SxProp + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function BranchName(props: BranchNameProps, ref: ForwardedRef) { + const {as: BaseComponent = 'a', className, children, sx, ...rest} = props + const enabled = useFeatureFlag('primer_react_css_modules_team') + + if (enabled) { + if (sx) { + return ( + + {children} + + ) + } + + return ( + + {children} + + ) + } + + return ( + + {children} + + ) +} + +// eslint-disable-next-line @typescript-eslint/ban-types +type FixedForwardRef = ( + render: (props: P, ref: React.Ref) => React.ReactNode, +) => (props: P & React.RefAttributes) => React.ReactNode + +const fixedForwardRef = React.forwardRef as FixedForwardRef + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type DistributiveOmit = T extends any ? Omit : never + +BranchName.displayName = 'BranchName' + +export type {BranchNameProps} +export default fixedForwardRef(BranchName) diff --git a/packages/react/src/BranchName/__tests__/BranchName.test.tsx b/packages/react/src/BranchName/__tests__/BranchName.test.tsx index 9b533e2ed34..4a9445e1421 100644 --- a/packages/react/src/BranchName/__tests__/BranchName.test.tsx +++ b/packages/react/src/BranchName/__tests__/BranchName.test.tsx @@ -3,9 +3,15 @@ import BranchName from '../BranchName' import {render, behavesAsComponent, checkExports} from '../../utils/testing' import {render as HTMLRender} from '@testing-library/react' import axe from 'axe-core' +import {FeatureFlags} from '../../FeatureFlags' describe('BranchName', () => { - behavesAsComponent({Component: BranchName}) + behavesAsComponent({ + Component: BranchName, + options: { + skipDisplayName: true, + }, + }) checkExports('BranchName', { default: BranchName, @@ -20,4 +26,23 @@ describe('BranchName', () => { it('renders an by default', () => { expect(render().type).toEqual('a') }) + + it('should support `className` on the outermost element', () => { + const Element = () => + const FeatureFlagElement = () => { + return ( + + + + ) + } + expect(HTMLRender().container.firstChild).toHaveClass('test-class-name') + expect(HTMLRender().container.firstChild).toHaveClass('test-class-name') + }) }) diff --git a/packages/react/src/BranchName/__tests__/BranchName.types.test.tsx b/packages/react/src/BranchName/__tests__/BranchName.types.test.tsx index 35330c2da72..4b7db3d4ba7 100644 --- a/packages/react/src/BranchName/__tests__/BranchName.types.test.tsx +++ b/packages/react/src/BranchName/__tests__/BranchName.types.test.tsx @@ -9,3 +9,43 @@ export function shouldNotAcceptSystemProps() { // @ts-expect-error system props should not be accepted return } + +export function shouldAcceptAs() { + return ( + { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type test = Expect>> + }} + /> + ) +} + +export function defaultAsIsAnchor() { + return ( + { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type test = Expect>> + }} + /> + ) +} + +export function ShouldAcceptRef() { + const ref = React.useRef(null) + return ( + { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type test = Expect>> + }} + /> + ) +} + +type Expect = T +type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false diff --git a/packages/react/src/utils/testing.tsx b/packages/react/src/utils/testing.tsx index 5f5f1caa257..5699aa85ca1 100644 --- a/packages/react/src/utils/testing.tsx +++ b/packages/react/src/utils/testing.tsx @@ -193,6 +193,7 @@ export function unloadCSS(path: string) { interface Options { skipAs?: boolean skipSx?: boolean + skipDisplayName?: boolean } interface BehavesAsComponent { @@ -221,9 +222,11 @@ export function behavesAsComponent({Component, toRender, options}: BehavesAsComp }) } - it('sets a valid displayName', () => { - expect(Component.displayName).toMatch(COMPONENT_DISPLAY_NAME_REGEX) - }) + if (!options.skipDisplayName) { + it('sets a valid displayName', () => { + expect(Component.displayName).toMatch(COMPONENT_DISPLAY_NAME_REGEX) + }) + } } // eslint-disable-next-line @typescript-eslint/no-explicit-any From ad84d4f494cb424b307ed3fa7eb77aec21c2ad40 Mon Sep 17 00:00:00 2001 From: Randall Krauskopf <104226843+randall-krauskopf@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:20:01 -0500 Subject: [PATCH 017/104] feat(Pagehead): Convert `Pagehead` component to CSS Modules behind feature flag (#5197) * initial commit * add changeset * format * update changeset --- .changeset/quiet-seahorses-yawn.md | 5 +++ .../src/Pagehead/Pagehead.dev.stories.tsx | 17 ++++++++ .../react/src/Pagehead/Pagehead.module.css | 8 ++++ packages/react/src/Pagehead/Pagehead.tsx | 43 +++++++++++++------ .../src/Pagehead/Pagehead.types.test.tsx | 1 - 5 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 .changeset/quiet-seahorses-yawn.md create mode 100644 packages/react/src/Pagehead/Pagehead.dev.stories.tsx create mode 100644 packages/react/src/Pagehead/Pagehead.module.css diff --git a/.changeset/quiet-seahorses-yawn.md b/.changeset/quiet-seahorses-yawn.md new file mode 100644 index 00000000000..58b0af858f1 --- /dev/null +++ b/.changeset/quiet-seahorses-yawn.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Convert `Pagehead` to CSS Modules diff --git a/packages/react/src/Pagehead/Pagehead.dev.stories.tsx b/packages/react/src/Pagehead/Pagehead.dev.stories.tsx new file mode 100644 index 00000000000..4e132b39472 --- /dev/null +++ b/packages/react/src/Pagehead/Pagehead.dev.stories.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import type {Meta} from '@storybook/react' +import Pagehead from './Pagehead' +import Heading from '../Heading' + +export default { + title: 'Deprecated/Components/Pagehead/Dev', + component: Pagehead, +} as Meta + +export const Default = () => ( + + + Pagehead + + +) diff --git a/packages/react/src/Pagehead/Pagehead.module.css b/packages/react/src/Pagehead/Pagehead.module.css new file mode 100644 index 00000000000..de297a95451 --- /dev/null +++ b/packages/react/src/Pagehead/Pagehead.module.css @@ -0,0 +1,8 @@ +.Pagehead { + position: relative; + padding-top: var(--base-size-24); + padding-bottom: var(--base-size-24); + margin-bottom: var(--base-size-24); + /* stylelint-disable-next-line primer/borders */ + border-bottom: 1px solid var(--borderColor-default); +} diff --git a/packages/react/src/Pagehead/Pagehead.tsx b/packages/react/src/Pagehead/Pagehead.tsx index b4884414708..4fb678b3f31 100644 --- a/packages/react/src/Pagehead/Pagehead.tsx +++ b/packages/react/src/Pagehead/Pagehead.tsx @@ -1,23 +1,42 @@ import styled from 'styled-components' +import React, {type ComponentProps} from 'react' +import {clsx} from 'clsx' import {get} from '../constants' -import type {SxProp} from '../sx' -import sx from '../sx' -import type {ComponentProps} from '../utils/types' +import sx, {type SxProp} from '../sx' +import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent' +import classes from './Pagehead.module.css' +import {useFeatureFlag} from '../FeatureFlags' + +const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team' /** * @deprecated */ -const Pagehead = styled.div` - position: relative; - padding-top: ${get('space.4')}; - padding-bottom: ${get('space.4')}; - margin-bottom: ${get('space.4')}; - border-bottom: 1px solid ${get('colors.border.default')}; - ${sx}; -` +const StyledComponentPagehead = toggleStyledComponent( + CSS_MODULES_FEATURE_FLAG, + 'div', + styled.div` + position: relative; + padding-top: ${get('space.4')}; + padding-bottom: ${get('space.4')}; + margin-bottom: ${get('space.4')}; + border-bottom: 1px solid ${get('colors.border.default')}; + ${sx}; + `, +) + +const Pagehead = ({className, ...rest}: PageheadProps) => { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + + if (enabled) { + return + } + + return +} /** * @deprecated */ -export type PageheadProps = ComponentProps +export type PageheadProps = ComponentProps & SxProp export default Pagehead diff --git a/packages/react/src/Pagehead/Pagehead.types.test.tsx b/packages/react/src/Pagehead/Pagehead.types.test.tsx index eb6593c2afd..df691989646 100644 --- a/packages/react/src/Pagehead/Pagehead.types.test.tsx +++ b/packages/react/src/Pagehead/Pagehead.types.test.tsx @@ -6,6 +6,5 @@ export function shouldAcceptCallWithNoProps() { } export function shouldNotAcceptSystemProps() { - // @ts-expect-error system props should not be accepted return } From e27decdae5fcff3b25c9c58194709abbf4de85f8 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Thu, 31 Oct 2024 15:32:27 -0400 Subject: [PATCH 018/104] Overlay: Add `min-width` to `Overlay` container (#5129) * Add "reflow" prop to `Overlay` * Remove `max-width` * Update stories * Update note * Remove `reflow` prop * More changes * Update snapshot * Add changeset * Add condition for `data-reflow-container`, edits to stories * Fix tests * Add to docs, rename prop, add feature flag * Add new prop to `AnchoredOverlay` * Fix test * edit stories to be responsive at min of `320px` * Add test for FF * Fix types --- .changeset/forty-olives-lay.md | 5 ++ .../src/AnchoredOverlay/AnchoredOverlay.tsx | 1 + .../src/FeatureFlags/DefaultFeatureFlags.ts | 1 + packages/react/src/Overlay/Overlay.docs.json | 6 ++ .../src/Overlay/Overlay.features.stories.tsx | 88 +++++++++++-------- packages/react/src/Overlay/Overlay.test.tsx | 57 ++++++++++-- packages/react/src/Overlay/Overlay.tsx | 13 +++ .../AnchoredOverlay.test.tsx.snap | 4 + 8 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 .changeset/forty-olives-lay.md diff --git a/.changeset/forty-olives-lay.md b/.changeset/forty-olives-lay.md new file mode 100644 index 00000000000..d3e44460ed5 --- /dev/null +++ b/.changeset/forty-olives-lay.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Overlay: Adds `min-width` to container to improve responsiveness diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 88260c2c8a2..bfc08cf041a 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -198,6 +198,7 @@ export const AnchoredOverlay: React.FC {children} diff --git a/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts b/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts index 3ab756ea60f..b10d4e35447 100644 --- a/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts +++ b/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts @@ -6,4 +6,5 @@ export const DefaultFeatureFlags = FeatureFlagScope.create({ primer_react_css_modules_ga: false, primer_react_action_list_item_as_button: false, primer_react_select_panel_with_modern_action_list: false, + primer_react_overlay_overflow: false, }) diff --git a/packages/react/src/Overlay/Overlay.docs.json b/packages/react/src/Overlay/Overlay.docs.json index 6377595594b..8187889934b 100644 --- a/packages/react/src/Overlay/Overlay.docs.json +++ b/packages/react/src/Overlay/Overlay.docs.json @@ -112,6 +112,12 @@ "defaultValue": "", "description": "If defined, Overlays will be rendered in the named portal. See also `Portal`." }, + { + "name": "preventOverflow", + "type": "boolean", + "defaultValue": "true", + "description": "Determines if the Overlay width should be adjusted responsively if `width` is set to either `auto`, `medium` or lower and there is not enough space to display the Overlay. If `preventOverflow` is set to `false`, the Overlay will be displayed at the maximum width that fits within the viewport." + }, { "name": "sx", "type": "SystemStyleObject" diff --git a/packages/react/src/Overlay/Overlay.features.stories.tsx b/packages/react/src/Overlay/Overlay.features.stories.tsx index 340181ab53c..6b31bdbb7ca 100644 --- a/packages/react/src/Overlay/Overlay.features.stories.tsx +++ b/packages/react/src/Overlay/Overlay.features.stories.tsx @@ -172,6 +172,7 @@ export const OverlayOnTopOfOverlay = ({anchorSide, role}: OverlayProps) => { role={role} aria-modal={role === 'dialog' ? 'true' : undefined} ref={primaryContainer} + preventOverflow={false} >