diff --git a/.changeset/chilled-spoons-roll.md b/.changeset/chilled-spoons-roll.md new file mode 100644 index 00000000000..61e35476d5c --- /dev/null +++ b/.changeset/chilled-spoons-roll.md @@ -0,0 +1,5 @@ +--- +"@primer/react": major +--- + +Chore/remove styled. components: deprecated UnderlineNav, ValidationAnimation, LabelGroup, Tooltip diff --git a/packages/react/src/BranchName/BranchName.tsx b/packages/react/src/BranchName/BranchName.tsx index a6e84905cbe..aeb7bf4ff20 100644 --- a/packages/react/src/BranchName/BranchName.tsx +++ b/packages/react/src/BranchName/BranchName.tsx @@ -1,12 +1,16 @@ -import React, {type ForwardedRef} from 'react' +import type React from 'react' +import type {ForwardedRef} from 'react' import {clsx} from 'clsx' import classes from './BranchName.module.css' +import {fixedForwardRef, type PolymorphicProps} from '../utils/modern-polymorphic' -type BranchNameProps = { - as?: As -} & DistributiveOmit, 'as'> & { +export type BranchNameProps = PolymorphicProps< + As, + 'a', + { className?: string } +> // eslint-disable-next-line @typescript-eslint/no-explicit-any function BranchName(props: BranchNameProps, ref: ForwardedRef) { @@ -18,17 +22,6 @@ function BranchName(props: BranchNameProps, re ) } -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -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.types.test.tsx b/packages/react/src/BranchName/__tests__/BranchName.types.test.tsx index f8b4543e614..0831b5691d9 100644 --- a/packages/react/src/BranchName/__tests__/BranchName.types.test.tsx +++ b/packages/react/src/BranchName/__tests__/BranchName.types.test.tsx @@ -24,7 +24,7 @@ export function shouldAcceptAs() { export function defaultAsIsAnchor() { return ( { + onClick={(event: React.MouseEvent) => { type test = Expect>> }} /> diff --git a/packages/react/src/DialogV1/Dialog.test.tsx b/packages/react/src/DialogV1/Dialog.test.tsx index 3eb7d6912e5..68dcf19fa1d 100644 --- a/packages/react/src/DialogV1/Dialog.test.tsx +++ b/packages/react/src/DialogV1/Dialog.test.tsx @@ -23,7 +23,7 @@ const Component = () => {
Title
- Some content + Some content
@@ -37,7 +37,7 @@ const ClosedDialog = () => {
Title
- Some content + Some content
@@ -51,7 +51,7 @@ const DialogWithCustomFocusRef = () => {
Title
- Some content + Some content @@ -80,7 +80,7 @@ const DialogWithCustomFocusRefAndReturnFocusRef = () => {
Title
- Some content + Some content diff --git a/packages/react/src/FormControl/FormControlLeadingVisual.tsx b/packages/react/src/FormControl/FormControlLeadingVisual.tsx index a0493309d2b..8f29d2fc6d4 100644 --- a/packages/react/src/FormControl/FormControlLeadingVisual.tsx +++ b/packages/react/src/FormControl/FormControlLeadingVisual.tsx @@ -1,5 +1,4 @@ import type React from 'react' -import {get} from '../constants' import type {SxProp} from '../sx' import {useFormControlContext} from './_FormControlContext' import styled from 'styled-components' @@ -24,7 +23,7 @@ const FormControlLeadingVisual: React.FC> = ({ children, visibleChildCount, overflowStyle = 'overlay', - as = 'ul', + as: Component = 'ul', className, }) => { const containerRef = React.useRef(null) @@ -320,28 +280,30 @@ const LabelGroup: React.FC> = ({ } }, [overflowStyle, isOverflowShown]) - const isList = as === 'ul' || as === 'ol' + const isList = Component === 'ul' || Component === 'ol' const ToggleWrapper = isList ? 'li' : React.Fragment + const ItemWrapperComponent = isList ? 'li' : 'span' + // If truncation is enabled, we need to render based on truncation logic. return visibleChildCount ? ( - {React.Children.map(children, (child, index) => ( - {child} - + ))} {overflowStyle === 'inline' ? ( @@ -369,15 +331,15 @@ const LabelGroup: React.FC> = ({ )} - + ) : ( - + {isList ? React.Children.map(children, (child, index) => { return
  • {child}
  • }) : children} -
    + ) } diff --git a/packages/react/src/PageLayout/PageLayout.examples.stories.tsx b/packages/react/src/PageLayout/PageLayout.examples.stories.tsx index 4605bb722e6..675119d4ac5 100644 --- a/packages/react/src/PageLayout/PageLayout.examples.stories.tsx +++ b/packages/react/src/PageLayout/PageLayout.examples.stories.tsx @@ -677,201 +677,6 @@ export const FiltersBottomSheetTwoLevels: StoryFn = () => { FiltersBottomSheetTwoLevels.storyName = 'Filters w/ 2 levels (btm sheet on narrow)' -// -// TODO: uncomment this story if we decide we want to allow this pattern for separate sets of filters -// -// export const ResponsiveNavCombo2: Story = () => { -// const [currentHash, setCurrentHash] = React.useState(window.location.hash) -// const [isOpen, setIsOpen] = React.useState(false) - -// const categories = [ -// { -// hash: '#fruits', -// name: 'Fruits', -// }, -// { -// hash: '#vegetables', -// name: 'Vegetables', -// }, -// { -// hash: '#animals', -// name: 'Animals', -// }, -// ] -// const selectedCategory = currentHash ? categories.find(option => currentHash.includes(option.hash)) : categories[0] - -// const buttonRef = React.useRef(null) - -// const onDialogClose = React.useCallback(() => setIsOpen(false), []) -// const getFilteredItems = (category: keyof typeof filterableItems) => -// filterableItems[category].filter(item => -// currentHash.includes('filter') ? currentHash.includes(`filter=${item.color}`) : true, -// ) - -// // Fake routing to mimic the behavior of a single page application -// React.useEffect(() => { -// const handleHashChange = () => { -// setCurrentHash(window.location.hash) -// } -// window.addEventListener('hashchange', handleHashChange) -// return () => { -// window.removeEventListener('hashchange', handleHashChange) -// } -// }, []) - -// return ( -// <> -// {/* -// Filters only work when you open the canvas in a new tab without the Storybook chrome. -// */} -// -// -// -// -// -// -// ) -// } - // ResponsiveNavCombo2.storyName = 'Responsive nav combo - action menu + btm sheet' ///////////////////////////////////////////////////////////////// diff --git a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx index eab5b81d777..d71f85231ee 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx @@ -139,10 +139,10 @@ IconOnly.storyName = 'Icon only' export const AssociatedWithALabelAndCaption = () => (
    - + File view - + Change the way the file is viewed
    diff --git a/packages/react/src/Skeleton/SkeletonBox.tsx b/packages/react/src/Skeleton/SkeletonBox.tsx index 3f747fdcfca..eb52dc7800b 100644 --- a/packages/react/src/Skeleton/SkeletonBox.tsx +++ b/packages/react/src/Skeleton/SkeletonBox.tsx @@ -2,7 +2,6 @@ import React from 'react' import {type CSSProperties, type HTMLProps} from 'react' import {clsx} from 'clsx' import classes from './SkeletonBox.module.css' -import {merge} from '../sx' export type SkeletonBoxProps = { /** Height of the skeleton "box". Accepts any valid CSS `height` value. */ @@ -21,13 +20,7 @@ export const SkeletonBox = React.forwardRef(funct
    } className={clsx(className, classes.SkeletonBox)} - style={merge( - style as CSSProperties, - { - height, - width, - } as CSSProperties, - )} + style={{height, width, ...(style || {})}} {...props} /> ) diff --git a/packages/react/src/SkeletonAvatar/SkeletonAvatar.tsx b/packages/react/src/SkeletonAvatar/SkeletonAvatar.tsx index 9d522862ea8..e3724053d5f 100644 --- a/packages/react/src/SkeletonAvatar/SkeletonAvatar.tsx +++ b/packages/react/src/SkeletonAvatar/SkeletonAvatar.tsx @@ -1,12 +1,10 @@ import type React from 'react' -import {type CSSProperties} from 'react' import {isResponsiveValue} from '../hooks/useResponsiveValue' import type {AvatarProps} from '../Avatar' import {DEFAULT_AVATAR_SIZE} from '../Avatar/Avatar' import {SkeletonBox} from '../Skeleton' import classes from './SkeletonAvatar.module.css' import {clsx} from 'clsx' -import {merge} from '../sx' interface SkeletonAvatarProps extends Omit, 'size'> { /** Class name for custom styling */ @@ -34,7 +32,7 @@ function SkeletonAvatar({size = DEFAULT_AVATAR_SIZE, square, className, style, . data-component="SkeletonAvatar" data-responsive={responsive ? '' : undefined} data-square={square ? '' : undefined} - style={merge(style as CSSProperties, cssSizeVars)} + style={{...(style || {}), ...cssSizeVars}} /> ) } diff --git a/packages/react/src/Text/Text.tsx b/packages/react/src/Text/Text.tsx index c3e63438856..6be84edb88d 100644 --- a/packages/react/src/Text/Text.tsx +++ b/packages/react/src/Text/Text.tsx @@ -1,38 +1,36 @@ import {clsx} from 'clsx' -import {type StyledComponent} from 'styled-components' -import React, {forwardRef} from 'react' -import type {SystemCommonProps, SystemTypographyProps} from '../constants' +import React, {type ForwardedRef} from 'react' import {useRefObjectAsForwardedRef} from '../hooks' import classes from './Text.module.css' +import {fixedForwardRef, type PolymorphicProps} from '../utils/modern-polymorphic' -type StyledTextProps = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - as?: React.ComponentType | keyof JSX.IntrinsicElements - size?: 'large' | 'medium' | 'small' - weight?: 'light' | 'normal' | 'medium' | 'semibold' -} & SystemTypographyProps & - SystemCommonProps & - React.HTMLAttributes +export type TextProps = PolymorphicProps< + As, + 'span', + { + size?: 'large' | 'medium' | 'small' + weight?: 'light' | 'normal' | 'medium' | 'semibold' + className?: string + } +> -const Text = forwardRef(({as: Component = 'span', className, size, weight, ...props}, forwardedRef) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function Text(props: TextProps, ref: ForwardedRef) { + const {as: Component = 'span', className, size, weight, ...rest} = props const innerRef = React.useRef(null) - useRefObjectAsForwardedRef(forwardedRef, innerRef) + useRefObjectAsForwardedRef(ref, innerRef) return ( ) - - // eslint-disable-next-line @typescript-eslint/no-explicit-any -}) as StyledComponent<'span', any, StyledTextProps, never> +} Text.displayName = 'Text' -export type TextProps = StyledTextProps -export default Text +export default fixedForwardRef(Text) diff --git a/packages/react/src/TextInput/TextInput.docs.json b/packages/react/src/TextInput/TextInput.docs.json index 66de3f3ac0a..ca52640677a 100644 --- a/packages/react/src/TextInput/TextInput.docs.json +++ b/packages/react/src/TextInput/TextInput.docs.json @@ -142,24 +142,24 @@ }, { "name": "width", - "type": "string | number | Array", + "type": "string | number", "defaultValue": "", "deprecated": true, - "description": "(Use sx prop) Set the width of the input" + "description": "(Use css modules) Set the width of the input" }, { "name": "maxWidth", - "type": "string | number | Array", + "type": "string | number", "defaultValue": "", "deprecated": true, - "description": "(Use sx prop) Set the maximum width of the input" + "description": "(Use css modules) Set the maximum width of the input" }, { "name": "minWidth", - "type": "string | number | Array", + "type": "string | number", "defaultValue": "", "deprecated": true, - "description": "(Use sx prop) Set the minimum width of the input" + "description": "(Use css modules) Set the minimum width of the input" }, { "name": "icon", diff --git a/packages/react/src/TextInput/TextInput.tsx b/packages/react/src/TextInput/TextInput.tsx index 9062b461411..a1bd78a6ee3 100644 --- a/packages/react/src/TextInput/TextInput.tsx +++ b/packages/react/src/TextInput/TextInput.tsx @@ -80,10 +80,10 @@ const TextInput = React.forwardRef( onFocus, onBlur, // start deprecated props + variant: variantProp, width: widthProp, minWidth: minWidthProp, maxWidth: maxWidthProp, - variant: variantProp, // end deprecated props type = 'text', required, diff --git a/packages/react/src/Tooltip/Tooltip.module.css b/packages/react/src/Tooltip/Tooltip.module.css new file mode 100644 index 00000000000..4a7266dc6d9 --- /dev/null +++ b/packages/react/src/Tooltip/Tooltip.module.css @@ -0,0 +1,161 @@ +/* stylelint-disable primer/typography, primer/borders, selector-class-pattern */ + +.Tooltip { + position: relative; + display: inline-block; +} + +.Tooltip::after { + position: absolute; + z-index: 1000000; + display: none; + /* stylelint-disable-next-line primer/spacing */ + padding: 0.5em 0.75em; + font: normal normal var(--text-body-size-small) / var(--text-body-lineHeight-small) var(--fontStack-system); + -webkit-font-smoothing: subpixel-antialiased; + color: var(--tooltip-fgColor, var(--fgColor-onEmphasis)); + text-align: center; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-wrap: break-word; + white-space: pre; + pointer-events: none; + content: attr(aria-label); + background: var(--tooltip-bgColor, var(--bgColor-emphasis)); + border-radius: var(--borderRadius-medium); + opacity: 0; +} + +/* delay animation for tooltip */ +@keyframes tooltip-appear { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.Tooltip:hover::after, +.Tooltip:active::after, +.Tooltip:focus::after, +.Tooltip:focus-within::after { + display: inline-block; + text-decoration: none; + animation-name: tooltip-appear; + animation-duration: 0.1s; + animation-fill-mode: forwards; + animation-timing-function: ease-in; + animation-delay: 0s; +} + +.Tooltip--noDelay:hover::after, +.Tooltip--noDelay:active::after, +.Tooltip--noDelay:focus::after, +.Tooltip--noDelay:focus-within::after { + animation-delay: 0s; +} + +.Tooltip--multiline:hover::after, +.Tooltip--multiline:active::after, +.Tooltip--multiline:focus::after, +.Tooltip--multiline:focus-within::after { + display: table-cell; +} + +/* Tooltipped south */ +.Tooltip--s::after, +.Tooltip--se::after, +.Tooltip--sw::after { + top: 100%; + right: 50%; + /* stylelint-disable-next-line primer/spacing */ + margin-top: 6px; +} + +.Tooltip--se::after { + right: auto; + left: 50%; + margin-left: calc(-1 * var(--base-size-16)); +} + +.Tooltip--sw::after { + margin-right: calc(-1 * var(--base-size-16)); +} + +/* Tooltips above the object */ +.Tooltip--n::after, +.Tooltip--ne::after, +.Tooltip--nw::after { + right: 50%; + bottom: 100%; + /* stylelint-disable-next-line primer/spacing */ + margin-bottom: 6px; +} + +.Tooltip--ne::after { + right: auto; + left: 50%; + margin-left: calc(-1 * var(--base-size-16)); +} + +.Tooltip--nw::after { + margin-right: calc(-1 * var(--base-size-16)); +} + +/* Move the tooltip body to the center of the object. */ +.Tooltip--s::after, +.Tooltip--n::after { + transform: translateX(50%); +} + +/* Tooltipped to the left */ +.Tooltip--w::after { + right: 100%; + bottom: 50%; + /* stylelint-disable-next-line primer/spacing */ + margin-right: 6px; + transform: translateY(50%); +} + +/* tooltipped to the right */ +.Tooltip--e::after { + bottom: 50%; + left: 100%; + /* stylelint-disable-next-line primer/spacing */ + margin-left: 6px; + transform: translateY(50%); +} + +.Tooltip--multiline::after { + width: max-content; + max-width: 250px; + word-wrap: break-word; + white-space: pre-line; + border-collapse: separate; +} + +.Tooltip--multiline.Tooltip--s::after, +.Tooltip--multiline.Tooltip--n::after { + right: auto; + left: 50%; + transform: translateX(-50%); +} + +.Tooltip--multiline.Tooltip--w::after, +.Tooltip--multiline.Tooltip--e::after { + right: 100%; +} + +.Tooltip--alignRight::after { + right: 0; + margin-right: 0; +} + +.Tooltip--alignLeft::after { + left: 0; + margin-left: 0; +} diff --git a/packages/react/src/Tooltip/Tooltip.tsx b/packages/react/src/Tooltip/Tooltip.tsx index 1701bb7528f..2dec29b89d8 100644 --- a/packages/react/src/Tooltip/Tooltip.tsx +++ b/packages/react/src/Tooltip/Tooltip.tsx @@ -1,187 +1,11 @@ import {clsx} from 'clsx' import React, {useMemo} from 'react' -import styled from 'styled-components' -import {get} from '../constants' -import type {ComponentProps} from '../utils/types' import {useId} from '../hooks' +import classes from './Tooltip.module.css' +import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' /* Tooltip v1 */ -const TooltipBase = styled.span` - position: relative; - display: inline-block; - - &::after { - position: absolute; - z-index: 1000000; - display: none; - padding: 0.5em 0.75em; - font: normal normal 11px/1.5 ${get('fonts.normal')}; - -webkit-font-smoothing: subpixel-antialiased; - color: var(--tooltip-fgColor, ${get('colors.fg.onEmphasis')}); - text-align: center; - text-decoration: none; - text-shadow: none; - text-transform: none; - letter-spacing: normal; - word-wrap: break-word; - white-space: pre; - pointer-events: none; - content: attr(aria-label); - background: var(--tooltip-bgColor, ${get('colors.neutral.emphasisPlus')}); - border-radius: ${get('radii.2')}; - opacity: 0; - } - - // delay animation for tooltip - @keyframes tooltip-appear { - from { - opacity: 0; - } - - to { - opacity: 1; - } - } - - &:hover, - &:active, - &:focus, - &:focus-within { - &::after { - display: inline-block; - text-decoration: none; - animation-name: tooltip-appear; - animation-duration: 0.1s; - animation-fill-mode: forwards; - animation-timing-function: ease-in; - animation-delay: 0s; - } - } - - &.tooltipped-no-delay:hover, - &.tooltipped-no-delay:active, - &.tooltipped-no-delay:focus, - &.tooltipped-no-delay:focus-within { - &::after { - animation-delay: 0s; - } - } - - &.tooltipped-multiline:hover, - &.tooltipped-multiline:active, - &.tooltipped-multiline:focus, - &.tooltipped-multiline:focus-within { - &::after { - display: table-cell; - } - } - - // Tooltipped south - &.tooltipped-s, - &.tooltipped-se, - &.tooltipped-sw { - &::after { - top: 100%; - right: 50%; - margin-top: 6px; - } - } - - &.tooltipped-se { - &::after { - right: auto; - left: 50%; - margin-left: -${get('space.3')}; - } - } - - &.tooltipped-sw::after { - margin-right: -${get('space.3')}; - } - - // Tooltips above the object - &.tooltipped-n, - &.tooltipped-ne, - &.tooltipped-nw { - &::after { - right: 50%; - bottom: 100%; - margin-bottom: 6px; - } - } - - &.tooltipped-ne { - &::after { - right: auto; - left: 50%; - margin-left: -${get('space.3')}; - } - } - - &.tooltipped-nw::after { - margin-right: -${get('space.3')}; - } - - // Move the tooltip body to the center of the object. - &.tooltipped-s::after, - &.tooltipped-n::after { - transform: translateX(50%); - } - - // Tooltipped to the left - &.tooltipped-w { - &::after { - right: 100%; - bottom: 50%; - margin-right: 6px; - transform: translateY(50%); - } - } - - // tooltipped to the right - &.tooltipped-e { - &::after { - bottom: 50%; - left: 100%; - margin-left: 6px; - transform: translateY(50%); - } - } - - &.tooltipped-multiline { - &::after { - width: max-content; - max-width: 250px; - word-wrap: break-word; - white-space: pre-line; - border-collapse: separate; - } - - &.tooltipped-s::after, - &.tooltipped-n::after { - right: auto; - left: 50%; - transform: translateX(-50%); - } - - &.tooltipped-w::after, - &.tooltipped-e::after { - right: 100%; - } - } - - &.tooltipped-align-right-2::after { - right: 0; - margin-right: 0; - } - - &.tooltipped-align-left-2::after { - left: 0; - margin-left: 0; - } -` - /** * @deprecated */ @@ -191,32 +15,41 @@ export type TooltipProps = { noDelay?: boolean align?: 'left' | 'right' wrap?: boolean -} & ComponentProps +} & React.ComponentProps<'span'> export const TooltipContext = React.createContext<{tooltipId?: string}>({}) /** * @deprecated */ -function Tooltip({direction = 'n', children, className, text, noDelay, align, wrap, id, ...rest}: TooltipProps) { +const Tooltip = React.forwardRef(function Tooltip( + {as: Component = 'span', direction = 'n', children, className, text, noDelay, align, wrap, id, ...rest}, + ref, +) { const tooltipId = useId(id) - const classes = clsx( - className, - `tooltipped-${direction}`, - align && `tooltipped-align-${align}-2`, - noDelay && 'tooltipped-no-delay', - wrap && 'tooltipped-multiline', - ) + const tooltipClasses = clsx(className, classes.Tooltip, classes[`Tooltip--${direction}`], { + [classes[`Tooltip--align${align === 'left' ? 'Left' : 'Right'}`]]: align, + [classes['Tooltip--noDelay']]: noDelay, + [classes['Tooltip--multiline']]: wrap, + // maintaining feature parity with old classes + [`tooltipped-${direction}`]: true, + [`tooltipped-align-${align === 'left' ? 'left' : 'right'}-2`]: align, + 'tooltipped-no-delay': noDelay, + 'tooltipped-multiline': wrap, + }) const value = useMemo(() => ({tooltipId}), [tooltipId]) return ( // This provider is used to check if an icon button is wrapped with tooltip or not. - + {children} - + ) +}) as PolymorphicForwardRefComponent<'span', TooltipProps> & { + alignments: string[] + directions: string[] } Tooltip.alignments = ['left', 'right'] diff --git a/packages/react/src/Truncate/Truncate.tsx b/packages/react/src/Truncate/Truncate.tsx index e9c3e27e9cd..27630d1fc30 100644 --- a/packages/react/src/Truncate/Truncate.tsx +++ b/packages/react/src/Truncate/Truncate.tsx @@ -1,6 +1,5 @@ import React from 'react' import {clsx} from 'clsx' -import type {MaxWidthProps} from 'styled-system' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import classes from './Truncate.module.css' @@ -8,7 +7,8 @@ type TruncateProps = React.HTMLAttributes & { title: string inline?: boolean expandable?: boolean -} & MaxWidthProps + maxWidth?: number | string +} const Truncate = React.forwardRef(function Truncate( {as: Component = 'div', children, className, title, inline, expandable, maxWidth = 125, style, ...rest}, diff --git a/packages/react/src/UnderlineNav/UnderlineNav.tsx b/packages/react/src/UnderlineNav/UnderlineNav.tsx index d36b799130b..796d7f053da 100644 --- a/packages/react/src/UnderlineNav/UnderlineNav.tsx +++ b/packages/react/src/UnderlineNav/UnderlineNav.tsx @@ -7,7 +7,6 @@ import type {ChildWidthArray, ResponsiveProps, ChildSize} from './types' import VisuallyHidden from '../_VisuallyHidden' import {moreBtnStyles, dividerStyles, menuItemStyles, baseMenuMinWidth} from './styles' import {UnderlineItemList, UnderlineWrapper, LoadingCounter, GAP} from '../internal/components/UnderlineTabbedInterface' -import styled from 'styled-components' import {Button} from '../Button' import {TriangleDownIcon} from '@primer/octicons-react' import {useOnEscapePress} from '../hooks/useOnEscapePress' @@ -41,12 +40,6 @@ export const MORE_BTN_WIDTH = 86 // The height is needed to make sure we don't have a layout shift when the more button is the only item in the nav. const MORE_BTN_HEIGHT = 45 -export const MoreMenuListItem = styled.li` - display: flex; - align-items: center; - height: ${MORE_BTN_HEIGHT}px; -` - const overflowEffect = ( navWidth: number, moreMenuWidth: number, @@ -337,7 +330,7 @@ export const UnderlineNav = forwardRef( {listItems} {menuItems.length > 0 && ( - +
  • {!onlyMenuVisible &&
    }
  • )}
    diff --git a/packages/react/src/deprecated/ActionList/Item.tsx b/packages/react/src/deprecated/ActionList/Item.tsx index 176491d8f7f..0b4a1ed7816 100644 --- a/packages/react/src/deprecated/ActionList/Item.tsx +++ b/packages/react/src/deprecated/ActionList/Item.tsx @@ -1,7 +1,6 @@ import {CheckIcon} from '@primer/octicons-react' import React, {useCallback} from 'react' import {isValidElementType} from 'react-is' -import {get} from '../../constants' import Truncate from '../../Truncate' import type {ItemInput} from './List' import {useTheme} from '../../ThemeProvider' @@ -243,7 +242,7 @@ export const Item = React.forwardRef((itemProps, ref) => { id={descriptionId} style={ { - '--description-container-margin-left': descriptionVariant === 'inline' ? get('space.2')(theme) : 0, + '--description-container-margin-left': descriptionVariant === 'inline' ? 'var(--base-size-8)' : 0, '--description-container-flex-basis': descriptionVariant === 'inline' ? 0 : 'auto', } as React.CSSProperties } diff --git a/packages/react/src/deprecated/UnderlineNav/UnderlineNav.module.css b/packages/react/src/deprecated/UnderlineNav/UnderlineNav.module.css new file mode 100644 index 00000000000..f4825c09be2 --- /dev/null +++ b/packages/react/src/deprecated/UnderlineNav/UnderlineNav.module.css @@ -0,0 +1,85 @@ +.UnderlineNav { + display: flex; + justify-content: space-between; + border-bottom: var(--borderWidth-thin) solid var(--borderColor-muted); + + &.UnderlineNav--right { + justify-content: flex-end; + } + + &.UnderlineNav--right .UnderlineNavItem { + margin-right: 0; + margin-left: var(--base-size-16); + } + + &.UnderlineNav--right .UnderlineNavActions { + flex: 1 1 auto; + } + + &.UnderlineNav--full { + display: block; + } + + .UnderlineNavBody { + display: flex; + /* stylelint-disable-next-line primer/spacing */ + margin-bottom: -1px; + } + + .UnderlineNavActions { + align-self: center; + } +} + +.UnderlineNavLink { + padding: var(--base-size-16) var(--base-size-8); + margin-right: var(--base-size-16); + font-size: var(--text-body-size-medium); + line-height: var(--text-title-lineHeight-large); + color: var(--fgColor-default); + text-align: center; + /* stylelint-disable-next-line primer/borders */ + border-bottom: 2px solid transparent; + text-decoration: none; + + /* fallback :focus state */ + &:focus:not(:disabled) { + box-shadow: none; + outline: 2px solid var(--fgColor-accent); + outline-offset: -8px; + + /* remove fallback :focus if :focus-visible is supported */ + &:not(:focus-visible) { + outline: solid 1px transparent; + } + } + + /* default focus state */ + &:focus-visible:not(:disabled) { + box-shadow: none; + outline: 2px solid var(--fgColor-accent); + outline-offset: -8px; + } +} + +.UnderlineNavLink:hover, +.UnderlineNavLink:focus { + color: var(--fgColor-default); + text-decoration: none; + border-bottom-color: var(--borderColor-muted); + transition: border-bottom-color 0.2s ease; +} + +.UnderlineNavLink:hover .UnderlineNavOcticon, +.UnderlineNavLink:focus .UnderlineNavOcticon { + color: var(--fgColor-muted); +} + +.UnderlineNavLink:where([data-selected]) { + color: var(--fgColor-default); + border-bottom-color: var(--underlineNav-borderColor-active); +} + +.UnderlineNavLink:where([data-selected]) .UnderlineNavOcticon { + color: var(--fgColor-default); +} diff --git a/packages/react/src/deprecated/UnderlineNav/UnderlineNav.tsx b/packages/react/src/deprecated/UnderlineNav/UnderlineNav.tsx index 2ecc4372336..96e11bf7a53 100644 --- a/packages/react/src/deprecated/UnderlineNav/UnderlineNav.tsx +++ b/packages/react/src/deprecated/UnderlineNav/UnderlineNav.tsx @@ -1,63 +1,27 @@ import {clsx} from 'clsx' import type {To} from 'history' -import type React from 'react' -import styled from 'styled-components' -import {get} from '../../constants' -import type {ComponentProps} from '../../utils/types' -import getGlobalFocusStyles from '../../internal/utils/getGlobalFocusStyles' - -const ITEM_CLASS = 'PRC-UnderlineNav-item' -const SELECTED_CLASS = 'PRC-selected' - -const UnderlineNavBase = styled.nav` - display: flex; - justify-content: space-between; - border-bottom: 1px solid ${get('colors.border.muted')}; - &.PRC-UnderlineNav--right { - justify-content: flex-end; - - .PRC-UnderlineNav-item { - margin-right: 0; - margin-left: ${get('space.3')}; - } - - .PRC-UnderlineNav-actions { - flex: 1 1 auto; - } - } - &.PRC-UnderlineNav--full { - display: block; - } - - .PRC-UnderlineNav-body { - display: flex; - margin-bottom: -1px; - } - - .PRC-UnderlineNav-actions { - align-self: center; - } -` +import React from 'react' +import classes from './UnderlineNav.module.css' export type UnderlineNavProps = { actions?: React.ReactNode align?: 'right' full?: boolean label?: string -} & ComponentProps - -function UnderlineNav({actions, className, align, children, full, label, theme, ...rest}: UnderlineNavProps) { - const classes = clsx( - className, - 'PRC-UnderlineNav', - align && `PRC-UnderlineNav--${align}`, - full && 'PRC-UnderlineNav--full', - ) +} & React.ComponentProps<'nav'> + +function UnderlineNav({actions, className, align, children, full, label, ...rest}: UnderlineNavProps) { + const navClasses = clsx(className, classes.UnderlineNav, 'PRC-UnderlineNav', { + [classes['UnderlineNav--right']]: align === 'right', + [classes['UnderlineNav--full']]: full, + 'PRC-UnderlineNav--full': full, + 'PRC-UnderlineNav--right': align, + }) return ( - -
    {children}
    - {actions &&
    {actions}
    } -
    + ) } @@ -66,45 +30,19 @@ type StyledUnderlineNavLinkProps = { selected?: boolean } -const UnderlineNavLink = styled.a.attrs(props => ({ - className: clsx(ITEM_CLASS, props.selected && SELECTED_CLASS, props.className), -}))` - padding: ${get('space.3')} ${get('space.2')}; - margin-right: ${get('space.3')}; - font-size: ${get('fontSizes.1')}; - line-height: ${get('lineHeights.default')}; - color: ${get('colors.fg.default')}; - text-align: center; - border-bottom: 2px solid transparent; - text-decoration: none; - - &:hover, - &:focus { - color: ${get('colors.fg.default')}; - text-decoration: none; - border-bottom-color: ${get('colors.neutral.muted')}; - transition: border-bottom-color 0.2s ease; - - .PRC-UnderlineNav-octicon { - color: ${get('colors.fg.muted')}; - } - } - - &.PRC-selected { - color: ${get('colors.fg.default')}; - border-bottom-color: ${get('colors.primer.border.active')}; - - .PRC-UnderlineNav-octicon { - color: ${get('colors.fg.default')}; - } - } +type UnderlineNavLinkProps = React.ComponentProps<'a'> & StyledUnderlineNavLinkProps - ${getGlobalFocusStyles('-8px')}; -` +const UnderlineNavLink = React.forwardRef(function UnderlineNavLink( + {className, selected, ...props}, + forwardRef, +) { + const linkClasses = clsx(classes.UnderlineNavItem, className, classes.UnderlineNavLink) + return +}) UnderlineNavLink.displayName = 'UnderlineNav.Link' -export type UnderlineNavLinkProps = ComponentProps +export type {UnderlineNavLinkProps} /** * @deprecated UnderlineNav is deprecated and will be replaced by the draft `UnderlineNav` in the next major release. See https://primer.style/react/drafts/UnderlineNav2 for more details. */ diff --git a/packages/react/src/experimental/UnderlinePanels/UnderlinePanels.tsx b/packages/react/src/experimental/UnderlinePanels/UnderlinePanels.tsx index 5067ae0f922..ece7047c3e4 100644 --- a/packages/react/src/experimental/UnderlinePanels/UnderlinePanels.tsx +++ b/packages/react/src/experimental/UnderlinePanels/UnderlinePanels.tsx @@ -20,7 +20,6 @@ import { } from '../../internal/components/UnderlineTabbedInterface' import {useId} from '../../hooks' import {invariant} from '../../utils/invariant' -import {type SxProp} from '../../sx' import {useResizeObserver, type ResizeObserverEntry} from '../../hooks/useResizeObserver' import useIsomorphicLayoutEffect from '../../utils/useIsomorphicLayoutEffect' import classes from './UnderlinePanels.module.css' @@ -51,7 +50,7 @@ export type UnderlinePanelsProps = { * Class name for custom styling */ className?: string -} & SxProp +} export type TabProps = PropsWithChildren<{ /** @@ -70,8 +69,7 @@ export type TabProps = PropsWithChildren<{ * Icon rendered before the tab text label */ icon?: FC -}> & - SxProp +}> export type PanelProps = React.HTMLAttributes diff --git a/packages/react/src/internal/components/TextInputWrapper.tsx b/packages/react/src/internal/components/TextInputWrapper.tsx index 4d7a1a9280a..753e6ede66a 100644 --- a/packages/react/src/internal/components/TextInputWrapper.tsx +++ b/packages/react/src/internal/components/TextInputWrapper.tsx @@ -1,5 +1,4 @@ import React, {type ComponentProps} from 'react' -import {type ResponsiveValue} from 'styled-system' import type {SxProp} from '../../sx' import type {FormValidationStatus} from '../../utils/types/FormValidationStatus' import {clsx} from 'clsx' @@ -25,11 +24,11 @@ type StyledTextInputBaseWrapperProps = { onClick?: React.MouseEventHandler children?: React.ReactNode /** @deprecated Update `width` using CSS modules or style. */ - width?: string | number | ResponsiveValue + width?: string | number /** @deprecated Update `min-width` using CSS modules or style. */ - minWidth?: string | number | ResponsiveValue + minWidth?: string | number /** @deprecated Update `max-width` using CSS modules or style. */ - maxWidth?: string | number | ResponsiveValue + maxWidth?: string | number } & SxProp type StyledTextInputWrapperProps = { @@ -72,11 +71,7 @@ export const TextInputBaseWrapper = React.forwardRef ) diff --git a/packages/react/src/internal/components/ValidationAnimationContainer.module.css b/packages/react/src/internal/components/ValidationAnimationContainer.module.css new file mode 100644 index 00000000000..32cbe1cbce6 --- /dev/null +++ b/packages/react/src/internal/components/ValidationAnimationContainer.module.css @@ -0,0 +1,19 @@ +.Animation:where([data-show]) { + animation: 170ms fadeIn cubic-bezier(0.44, 0.74, 0.36, 1); + + @media (prefers-reduced-motion) { + animation: none; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-100%); + } + + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/packages/react/src/internal/components/ValidationAnimationContainer.tsx b/packages/react/src/internal/components/ValidationAnimationContainer.tsx index 00e3f2cfa67..cb502eec431 100644 --- a/packages/react/src/internal/components/ValidationAnimationContainer.tsx +++ b/packages/react/src/internal/components/ValidationAnimationContainer.tsx @@ -1,29 +1,11 @@ import type {HTMLProps} from 'react' import type React from 'react' import {useEffect, useState} from 'react' -import styled, {keyframes, css} from 'styled-components' +import classes from './ValidationAnimationContainer.module.css' interface Props extends HTMLProps { show?: boolean } - -const fadeIn = keyframes` - 0% { - opacity: 0; - transform: translateY(-100%); - } - 100% { - opacity: 1; - transform: translateY(0); - } - ` -// using easeOutQuint easing fn https://easings.net/#easeOutQuint -const AnimatedElement = styled.div` - animation: ${props => props.show && css`170ms ${fadeIn} cubic-bezier(0.44, 0.74, 0.36, 1);`}; - @media (prefers-reduced-motion) { - animation: none; - } -` const ValidationAnimationContainer: React.FC> = ({show, children}) => { const [shouldRender, setRender] = useState(show) @@ -37,9 +19,9 @@ const ValidationAnimationContainer: React.FC> = ( return shouldRender ? (
    - +
    {children} - +
    ) : null } diff --git a/packages/react/src/internal/utils/sharedCheckboxAndRadioStyles.ts b/packages/react/src/internal/utils/sharedCheckboxAndRadioStyles.ts deleted file mode 100644 index d7a5fd33cd2..00000000000 --- a/packages/react/src/internal/utils/sharedCheckboxAndRadioStyles.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {css} from 'styled-components' -import {get} from '../../constants' - -export const sharedCheckboxAndRadioStyles = css` - appearance: none; - border-color: var(--control-borderColor-emphasis, ${get('colors.neutral.emphasis')}); - border-style: solid; - border-width: ${get('borderWidths.1')}; - cursor: pointer; - display: grid; - height: var(--base-size-16, 16px); - margin: 0; - margin-top: 0.125rem; /* 2px to center align with label (20px line-height) */ - place-content: center; - position: relative; - width: var(--base-size-16, 16px); - background-color: ${get('colors.canvas.default')}; - - &:disabled { - background-color: ${get('colors.input.disabledBg')}; - border-color: var(--control-borderColor-disabled, ${get('colors.border.default')}); - } -` diff --git a/packages/styled-react/src/components/Text.tsx b/packages/styled-react/src/components/Text.tsx index ee1c22a6385..efee4e79acf 100644 --- a/packages/styled-react/src/components/Text.tsx +++ b/packages/styled-react/src/components/Text.tsx @@ -2,20 +2,30 @@ import {Text as PrimerText, type TextProps as PrimerTextProps} from '@primer/rea import {sx, type SxProp} from '../sx' import styled from 'styled-components' import type React from 'react' -import {type StyledComponent} from 'styled-components' import {forwardRef} from 'react' +import type {ForwardRefComponent} from '../polymorphic' -type TextProps = PrimerTextProps & SxProp +// Create a base type without generics for styled-components +type BaseTextProps = { + size?: 'large' | 'medium' | 'small' + weight?: 'light' | 'normal' | 'medium' | 'semibold' + className?: string + children?: React.ReactNode + as?: React.ElementType +} & SxProp & + React.HTMLAttributes -const StyledText = styled(PrimerText).withConfig({ - shouldForwardProp: prop => (prop as keyof TextProps) !== 'sx', -})` +// Generic type that matches PrimerText exactly +type TextProps = PrimerTextProps & SxProp + +const StyledText = styled(PrimerText).withConfig({ + shouldForwardProp: prop => (prop as keyof BaseTextProps) !== 'sx', +})` ${sx} ` -const Text = forwardRef<'span', TextProps>(({as, ...props}, ref) => { +const Text = forwardRef(({as, ...props}, ref) => { return - // eslint-disable-next-line @typescript-eslint/no-explicit-any -}) as StyledComponent<'span', any, TextProps, never> +}) as ForwardRefComponent<'span', BaseTextProps> export {Text, type TextProps}