diff --git a/.changeset/loud-schools-own.md b/.changeset/loud-schools-own.md deleted file mode 100644 index 068d42c986a..00000000000 --- a/.changeset/loud-schools-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@primer/react": patch ---- - -Fix `TextInput` types diff --git a/.changeset/seven-hornets-jam.md b/.changeset/seven-hornets-jam.md deleted file mode 100644 index 2699e13c8dd..00000000000 --- a/.changeset/seven-hornets-jam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': minor ---- - -Adds button element selector to FilteredSearch button styles diff --git a/.changeset/spotty-eagles-help.md b/.changeset/spotty-eagles-help.md deleted file mode 100644 index 2593685d9e9..00000000000 --- a/.changeset/spotty-eagles-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': patch ---- - -Instead of rendering unexpected FormControl children before the rest of the content, we render them in the same spot we'd normally render a Primer input component diff --git a/.changeset/stupid-terms-hang.md b/.changeset/stupid-terms-hang.md deleted file mode 100644 index bee78b92e04..00000000000 --- a/.changeset/stupid-terms-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@primer/react": minor ---- - -Bump primer/primitives to `7.5.1` diff --git a/.changeset/ten-apes-smell.md b/.changeset/ten-apes-smell.md deleted file mode 100644 index 0485e9a3be5..00000000000 --- a/.changeset/ten-apes-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@primer/react": patch ---- - -Add overlay props to Autocomplete.Overlay diff --git a/.changeset/yellow-planes-decide.md b/.changeset/yellow-planes-decide.md deleted file mode 100644 index d0923ffb3ea..00000000000 --- a/.changeset/yellow-planes-decide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@primer/react": patch ---- - -Add disabled color and backgroundColor to Button.Counter diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d23bad7bf2..3ba19a02fcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # @primer/components +## 35.1.0 + +### Minor Changes + +- [#1942](https://github.com/primer/react/pull/1942) [`3f50ef54`](https://github.com/primer/react/commit/3f50ef543b8cea0306aba44bb44611f22dae657d) Thanks [@mperrotti](https://github.com/mperrotti)! - Adds button element selector to FilteredSearch button styles + +* [#1920](https://github.com/primer/react/pull/1920) [`40ed423e`](https://github.com/primer/react/commit/40ed423ed546ed91b69bc7bcc8361fd1e41faa8c) Thanks [@mperrotti](https://github.com/mperrotti)! - Adds a loadings state to our text input components + +- [#1961](https://github.com/primer/react/pull/1961) [`767d4166`](https://github.com/primer/react/commit/767d4166ef3e76e8ea12b6eec2d1d22f45f8609c) Thanks [@simurai](https://github.com/simurai)! - Bump primer/primitives to `7.5.1` + +### Patch Changes + +- [#1970](https://github.com/primer/react/pull/1970) [`3b236044`](https://github.com/primer/react/commit/3b23604438b850557e7e3d0a0594a8cca119859b) Thanks [@siddharthkp](https://github.com/siddharthkp)! - ActionMenu: Fix styles for windows high contrast mode + +* [#1981](https://github.com/primer/react/pull/1981) [`e9bb5956`](https://github.com/primer/react/commit/e9bb595680ff51a563260c020d1475102eba2535) Thanks [@mperrotti](https://github.com/mperrotti)! - Ensures select option text has acceptable contrast in Firefox when in dark mode + +- [#1945](https://github.com/primer/react/pull/1945) [`ef3b58a1`](https://github.com/primer/react/commit/ef3b58a1fdffc8d3f709c9f63e0ee70ee0f397ba) Thanks [@pksjce](https://github.com/pksjce)! - Icon button fixes: Removes iconLabel and adds aria-label to the type + +* [#1959](https://github.com/primer/react/pull/1959) [`2025036e`](https://github.com/primer/react/commit/2025036e552d8c8d02ed3139e5c8da4cb1546bb7) Thanks [@colebemis](https://github.com/colebemis)! - Fix `TextInput` types + +- [#1968](https://github.com/primer/react/pull/1968) [`1b01485a`](https://github.com/primer/react/commit/1b01485a282dc882aa7c8cc3a55fe736afdac029) Thanks [@mperrotti](https://github.com/mperrotti)! - Instead of rendering unexpected FormControl children before the rest of the content, we render them in the same spot we'd normally render a Primer input component + +* [#1967](https://github.com/primer/react/pull/1967) [`c83a06f0`](https://github.com/primer/react/commit/c83a06f00280ee2f8139d0cc3489242f1af46982) Thanks [@pksjce](https://github.com/pksjce)! - Add overlay props to Autocomplete.Overlay + +- [#1955](https://github.com/primer/react/pull/1955) [`77e123f4`](https://github.com/primer/react/commit/77e123f403df0669f492ac636de651506709bd9a) Thanks [@pksjce](https://github.com/pksjce)! - Add disabled color and backgroundColor to Button.Counter + ## 35.0.1 ### Patch Changes diff --git a/docs/content/ActionList.mdx b/docs/content/ActionList.mdx index 02c2fb4b032..9ee2161e640 100644 --- a/docs/content/ActionList.mdx +++ b/docs/content/ActionList.mdx @@ -1,7 +1,7 @@ --- componentId: action_list title: ActionList -status: Alpha +status: Beta source: https://github.com/primer/react/tree/main/src/ActionList storybook: '/react/storybook?path=/story/composite-components-actionlist' description: An ActionList is a list of items that can be activated or selected. ActionList is the base component for many menu-type components, including ActionMenu and SelectPanel. @@ -429,9 +429,9 @@ render() usedInProduction: true, usageExamplesDocumented: true, hasStorybookStories: true, - designReviewed: false, - a11yReviewed: false, - stableApi: false, + designReviewed: true, + a11yReviewed: true, + stableApi: false, // TODO: revisit on April 10, 2022 addressedApiFeedback: false, hasDesignGuidelines: true, hasFigmaComponent: true diff --git a/docs/content/Button.mdx b/docs/content/Button.mdx index e3adc9f2921..eeb8c234b7b 100644 --- a/docs/content/Button.mdx +++ b/docs/content/Button.mdx @@ -2,26 +2,26 @@ componentId: button title: Button status: Alpha -source: https://github.com/primer/react/tree/main/src/Button2 -storybook: '/react/storybook?path=/story/composite-components-button2' +source: https://github.com/primer/react/tree/main/src/Button +storybook: '/react/storybook?path=/story/composite-components-button' description: Use button for the main actions on a page or form. --- -import {Button, IconButton, LinkButton} from '@primer/react/drafts' +import {Button, IconButton, LinkButton} from '@primer/react' ## Usage ### Installation ```js -import {Button} from '@primer/react/drafts' +import {Button} from '@primer/react' ``` ### Default button This is the default variant for the `Button` component. -```jsx live drafts +```jsx live ``` @@ -29,7 +29,7 @@ This is the default variant for the `Button` component. The `danger` variant of `Button` is used to warn users about potentially destructive actions -```jsx live drafts +```jsx live ``` @@ -37,7 +37,7 @@ The `danger` variant of `Button` is used to warn users about potentially destruc The `outline` variant of `Button` is typically used as a secondary button -```jsx live drafts +```jsx live ``` @@ -45,7 +45,7 @@ The `outline` variant of `Button` is typically used as a secondary button The `invisible` variant of `Button` indicates that the action is a low priority one. -```jsx live drafts +```jsx live ``` @@ -53,7 +53,7 @@ The `invisible` variant of `Button` indicates that the action is a low priority `Button` component supports three different sizes. `small`, `medium`, `large`. -```jsx live drafts +```jsx live <> @@ -65,10 +65,10 @@ The `invisible` variant of `Button` indicates that the action is a low priority ### Appending an icon -We can place an inside the `Button` in either the leading or the trailing position to enhance the visual context. +We can place an icon inside the `Button` in either the leading or the trailing position to enhance the visual context. It is recommended to use an octicon here. -```jsx live drafts +```jsx live <> ``` diff --git a/docs/content/IconButton.mdx b/docs/content/IconButton.mdx index 0edea279283..2af6b14024b 100644 --- a/docs/content/IconButton.mdx +++ b/docs/content/IconButton.mdx @@ -2,8 +2,8 @@ title: IconButton componentId: icon_button status: Alpha -source: https://github.com/primer/react/tree/main/src/Button2 -storybook: '/react/storybook?path=/story/composite-components-button2' +source: https://github.com/primer/react/tree/main/src/Button +storybook: '/react/storybook?path=/story/composite-components-button' description: An accessible button component with no text and only icon. --- @@ -12,32 +12,26 @@ description: An accessible button component with no text and only icon. ### Installation ```js -import {IconButton} from '@primer/react/drafts' +import {IconButton} from '@primer/react' ``` ### Icon only button A separate component called `IconButton` is used if the action shows only an icon with no text. This button will remain square in shape. -```jsx live drafts -Search +```jsx live + ``` ### Different sized icon buttons `IconButton` also supports the three different sizes. `small`, `medium`, `large`. -```jsx live drafts +```jsx live <> - - Search - - - Search - - - Search - + + + ``` diff --git a/docs/content/TextInput.mdx b/docs/content/TextInput.mdx index b3bdfe57be6..bb98bb7113c 100644 --- a/docs/content/TextInput.mdx +++ b/docs/content/TextInput.mdx @@ -9,13 +9,15 @@ TextInput is a form component to add default styling to the native text input. **Note:** Don't forget to set `aria-label` to make the TextInput accessible to screen reader users. -## Default example +## Examples + +### Basic ```jsx live ``` -## Text Input with icons +### With icons ```jsx live <> @@ -37,7 +39,7 @@ TextInput is a form component to add default styling to the native text input. ``` -## Text Input with text visuals +### With text visuals ```jsx live <> @@ -47,7 +49,66 @@ TextInput is a form component to add default styling to the native text input. ``` -## Text Input with error and warning states +### With visuals and loading indicators + +```javascript live noinline +const WithIconAndLoadingIndicator = () => { + const [loading, setLoading] = React.useState(true) + + const toggleLoadingState = () => { + setLoading(!loading) + } + + return ( + <> + + + + + + No visual + + + + + + + Leading visual + + + + + + + Trailing visual + + + + + + + Both visuals + + + + + + + Both visuals, position overriden + + + + + + ) +} + +render() +``` + +### With error and warning states ```jsx live <> @@ -69,19 +130,19 @@ TextInput is a form component to add default styling to the native text input. ``` -## Block text input +### Block text input ```jsx live ``` -## Contrast text input +### Contrast text input ```jsx live ``` -## Monospace text input +### Monospace text input ```jsx live - Creates a full width input element - - } + description="Creates a full-width input element" /> - + + +
Which position to render the loading indicator
+
    +
  • + 'auto' (default): at the end of the input, unless a `leadingVisual` is passed. Then, it will render at the + beginning +
  • +
  • 'leading': at the beginning of the input
  • +
  • 'trailing': at the end of the input
  • +
+ + } +/> string | React.ComponentType} description="Visual positioned on the left edge inside the input" /> + string | React.ComponentType} @@ -138,7 +208,6 @@ TextInput is a form component to add default styling to the native text input. type="'error' | 'success' | 'warning'" description="Style the input to match the status" /> - { render(LeadingVisualExample) ``` +## With visuals and loading indicators + +```javascript live noinline +const WithIconAndLoadingIndicator = () => { + const [dates, setDates] = React.useState([ + {text: '01 Jan', id: 0}, + {text: '01 Feb', id: 1}, + {text: '01 Mar', id: 2} + ]) + const onDateRemove = tokenId => { + setDates(dates.filter(token => token.id !== tokenId)) + } + + const [loading, setLoading] = React.useState(true) + const toggleLoadingState = () => { + setLoading(!loading) + } + + return ( + <> + + + + +

No visual

+ + + + + + + + + + +

Leading visual

+ + + + + + + + + + +

Trailing visual

+ + + + + + + + + + +

Both visuals

+ + + + + + + + + + + ) +} + +render() +``` + ## Props diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml index 59369018686..0637cea346a 100644 --- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml +++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml @@ -89,8 +89,6 @@ url: /LabelGroup - title: Link url: /Link - - title: LinkButton - url: /LinkButton - title: Overlay url: /Overlay - title: Pagehead diff --git a/package.json b/package.json index 1570c01b181..f80909698e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@primer/react", - "version": "35.0.1", + "version": "35.1.0", "description": "An implementation of GitHub's Primer Design System using React", "main": "lib/index.js", "module": "lib-esm/index.js", diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 31a74a1ac52..c57eba32b85 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -161,8 +161,7 @@ export const Item = React.forwardRef( '@media (forced-colors: active)': { ':focus': { - // we set color to be transparent and let the high contrast rules - // decide what color with contrast should that be corrected to + // Support for Windows high contrast https://sarahmhigley.com/writing/whcm-quick-tips outline: 'solid 1px transparent !important' } }, diff --git a/src/Button/Button2.stories.tsx b/src/Button/Button.stories.tsx similarity index 92% rename from src/Button/Button2.stories.tsx rename to src/Button/Button.stories.tsx index a4ae611a3ba..800915dc3a9 100644 --- a/src/Button/Button2.stories.tsx +++ b/src/Button/Button.stories.tsx @@ -6,7 +6,7 @@ import {BaseStyles, ThemeProvider} from '..' import Box from '../Box' export default { - title: 'Composite components/Button2', + title: 'Composite components/Button', decorators: [ Story => { @@ -75,19 +75,19 @@ export const iconButton = ({...args}: ButtonProps) => { return ( <> - + - + - + - + - + ) @@ -193,7 +193,7 @@ export const DisabledButton = ({...args}: ButtonProps) => { - } iconLabel="Close" {...args} /> + } aria-label="Close" {...args} /> ) diff --git a/src/Button/IconButton.tsx b/src/Button/IconButton.tsx index 2b7ad955e48..401650829fc 100644 --- a/src/Button/IconButton.tsx +++ b/src/Button/IconButton.tsx @@ -4,11 +4,9 @@ import {useTheme} from '../ThemeProvider' import Box from '../Box' import {IconButtonProps, StyledButton} from './types' import {getBaseStyles, getSizeStyles, getVariantStyles} from './styles' -import {useSSRSafeId} from '@react-aria/ssr' const IconButton = forwardRef((props, forwardedRef): JSX.Element => { - const {variant = 'default', size = 'medium', sx: sxProp = {}, icon: Icon, iconLabel, ...rest} = props - const iconLabelId = useSSRSafeId() + const {variant = 'default', size = 'medium', sx: sxProp = {}, icon: Icon, ...rest} = props const {theme} = useTheme() const sxStyles = merge.all([ getBaseStyles(theme), @@ -17,12 +15,7 @@ const IconButton = forwardRef((props, forwar sxProp as SxProp ]) return ( - - {iconLabel && ( - - )} + diff --git a/src/Button/styles.ts b/src/Button/styles.ts index a17f6910347..a38b80b0b61 100644 --- a/src/Button/styles.ts +++ b/src/Button/styles.ts @@ -250,6 +250,12 @@ export const getBaseStyles = (theme?: Theme) => ({ }, '&:disabled svg': { opacity: '0.6' + }, + '@media (forced-colors: active)': { + '&:focus': { + // Support for Windows high contrast https://sarahmhigley.com/writing/whcm-quick-tips + outline: 'solid 1px transparent' + } } }) diff --git a/src/Button/types.ts b/src/Button/types.ts index 9f391f7b122..10c7188bc1a 100644 --- a/src/Button/types.ts +++ b/src/Button/types.ts @@ -11,6 +11,8 @@ export type Size = 'small' | 'medium' | 'large' type StyledButtonProps = ComponentPropsWithRef +type ButtonA11yProps = {'aria-label': string; 'aria-labelby'?: never} | {'aria-label'?: never; 'aria-labelby': string} + export type ButtonBaseProps = { /** * Determine's the styles on a button one of 'default' | 'primary' | 'invisible' | 'danger' @@ -40,12 +42,8 @@ export type ButtonProps = { children: React.ReactNode } & ButtonBaseProps -export type IconButtonProps = { - /** - * This is to be used if it is an icon-only button. Will make text visually hidden - */ +export type IconButtonProps = ButtonA11yProps & { icon: React.FunctionComponent - iconLabel: string } & ButtonBaseProps // adopted from React.AnchorHTMLAttributes diff --git a/src/Overlay.tsx b/src/Overlay.tsx index 6962a1f6112..cc9c721213c 100644 --- a/src/Overlay.tsx +++ b/src/Overlay.tsx @@ -78,6 +78,12 @@ const StyledOverlay = styled.div` :focus { outline: none; } + + @media (forced-colors: active) { + /* Support for Windows high contrast https://sarahmhigley.com/writing/whcm-quick-tips */ + outline: solid 1px transparent; + } + ${sx}; ` type BaseOverlayProps = { diff --git a/src/Select.tsx b/src/Select.tsx index 25708efa576..6469abab957 100644 --- a/src/Select.tsx +++ b/src/Select.tsx @@ -16,15 +16,14 @@ const StyledSelect = styled.select` outline: none; width: 100%; + option { + color: initial; + } + /* colors the select input's placeholder text */ &:invalid { color: ${get('colors.fg.subtle')}; } - - /* For Firefox: reverts color of non-placeholder options in the dropdown */ - &:invalid option:not(:first-child) { - color: ${get('colors.fg.default')}; - } ` const ArrowIndicatorSVG: React.FC<{className?: string}> = ({className}) => ( diff --git a/src/TextInput.tsx b/src/TextInput.tsx index 6c1a21d8e6d..08f59f3ea89 100644 --- a/src/TextInput.tsx +++ b/src/TextInput.tsx @@ -1,14 +1,32 @@ +import React, {MouseEventHandler} from 'react' import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic' import classnames from 'classnames' -import React from 'react' + +import TextInputInnerVisualSlot from './_TextInputInnerVisualSlot' +import {useProvidedRefOrCreate} from './hooks' import {Merge} from './utils/types' import TextInputWrapper, {StyledWrapperProps} from './_TextInputWrapper' import UnstyledTextInput from './_UnstyledTextInput' -type NonPassthroughProps = { +export type TextInputNonPassthroughProps = { /** @deprecated Use `leadingVisual` or `trailingVisual` prop instead */ icon?: React.ComponentType<{className?: string}> + /** Whether the to show a loading indicator in the input */ + loading?: boolean + /** + * Which position to render the loading indicator + * 'auto' (default): at the end of the input, unless a `leadingVisual` is passed. Then, it will render at the beginning + * 'leading': at the beginning of the input + * 'trailing': at the end of the input + **/ + loaderPosition?: 'auto' | 'leading' | 'trailing' + /** + * A visual that renders inside the input before the typing area + */ leadingVisual?: string | React.ComponentType<{className?: string}> + /** + * A visual that renders inside the input after the typing area + */ trailingVisual?: string | React.ComponentType<{className?: string}> } & Pick< StyledWrapperProps, @@ -25,7 +43,7 @@ type NonPassthroughProps = { | 'validationStatus' > -export type TextInputProps = Merge, NonPassthroughProps> +export type TextInputProps = Merge, TextInputNonPassthroughProps> // using forwardRef is important so that other components (ex. SelectMenu) can autofocus the input const TextInput = React.forwardRef( @@ -38,6 +56,8 @@ const TextInput = React.forwardRef( className, contrast, disabled, + loading, + loaderPosition, monospace, validationStatus, sx: sxProp, @@ -52,8 +72,16 @@ const TextInput = React.forwardRef( }, ref ) => { + const inputRef = useProvidedRefOrCreate(ref as React.RefObject) // this class is necessary to style FilterSearch, plz no touchy! const wrapperClasses = classnames(className, 'TextInput-wrapper') + const showLeadingLoadingIndicator = + loading && (loaderPosition === 'leading' || Boolean(LeadingVisual && loaderPosition !== 'trailing')) + const showTrailingLoadingIndicator = + loading && (loaderPosition === 'trailing' || Boolean(loaderPosition === 'auto' && !LeadingVisual)) + const focusInput: MouseEventHandler = () => { + inputRef.current?.focus() + } return ( ( minWidth={minWidthProp} maxWidth={maxWidthProp} variant={variantProp} - hasLeadingVisual={Boolean(LeadingVisual)} - hasTrailingVisual={Boolean(TrailingVisual)} + hasLeadingVisual={Boolean(LeadingVisual || showLeadingLoadingIndicator)} + hasTrailingVisual={Boolean(TrailingVisual || showTrailingLoadingIndicator)} + onClick={focusInput} + aria-live="polite" + aria-busy={Boolean(loading)} > {IconComponent && } - {LeadingVisual && ( - - {typeof LeadingVisual === 'function' ? : LeadingVisual} - - )} - - {TrailingVisual && ( - - {typeof TrailingVisual === 'function' ? : TrailingVisual} - - )} + + {typeof LeadingVisual === 'function' ? : LeadingVisual} + + + + {typeof TrailingVisual === 'function' ? : TrailingVisual} + ) } ) as PolymorphicForwardRefComponent<'input', TextInputProps> TextInput.defaultProps = { - type: 'text' + type: 'text', + loaderPosition: 'auto' } TextInput.displayName = 'TextInput' diff --git a/src/TextInputWithTokens.tsx b/src/TextInputWithTokens.tsx index f43c19f606f..4ab12930433 100644 --- a/src/TextInputWithTokens.tsx +++ b/src/TextInputWithTokens.tsx @@ -10,6 +10,7 @@ import Text from './Text' import {TextInputProps} from './TextInput' import Token from './Token/Token' import {TokenSizeKeys} from './Token/TokenBase' +import TextInputInnerVisualSlot from './_TextInputInnerVisualSlot' import TextInputWrapper, {textInputHorizPadding, TextInputSizes} from './_TextInputWrapper' import UnstyledTextInput from './_UnstyledTextInput' @@ -67,6 +68,8 @@ function TextInputWithTokensInnerComponent {IconComponent && !LeadingVisual && } - {LeadingVisual && !IconComponent && ( - - {typeof LeadingVisual === 'function' ? : LeadingVisual} - - )} + + {typeof LeadingVisual === 'function' ? : LeadingVisual} + } display="flex" @@ -346,11 +355,13 @@ function TextInputWithTokensInnerComponent ) : null} - {TrailingVisual && ( - - {typeof TrailingVisual === 'function' ? : TrailingVisual} - - )} + + {typeof TrailingVisual === 'function' ? : TrailingVisual} + ) } @@ -361,7 +372,8 @@ TextInputWithTokens.defaultProps = { tokenComponent: Token, size: 'extralarge', hideTokenRemoveButtons: false, - preventTokenWrapping: false + preventTokenWrapping: false, + loaderPosition: 'auto' } TextInputWithTokens.displayName = 'TextInputWithTokens' diff --git a/src/_TextInputInnerVisualSlot.tsx b/src/_TextInputInnerVisualSlot.tsx new file mode 100644 index 00000000000..aac487b11e8 --- /dev/null +++ b/src/_TextInputInnerVisualSlot.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import {Box, Spinner} from '.' +import {TextInputNonPassthroughProps} from './TextInput' + +const TextInputInnerVisualSlot: React.FC<{ + /** Whether the input is expected to ever show a loading indicator */ + hasLoadingIndicator: boolean + /** Whether the to show the loading indicator */ + showLoadingIndicator: TextInputNonPassthroughProps['loading'] + /** Which side of this visual is being rendered */ + visualPosition: 'leading' | 'trailing' +}> = ({children, hasLoadingIndicator, showLoadingIndicator, visualPosition}) => { + if ((!children && !hasLoadingIndicator) || (visualPosition === 'leading' && !children && !showLoadingIndicator)) { + return null + } + + if (!hasLoadingIndicator) { + return {children} + } + + return ( + + + {children && {children}} + + + + ) +} + +export default TextInputInnerVisualSlot diff --git a/src/__tests__/Button.test.tsx b/src/__tests__/Button.test.tsx index 4da60203806..7e917445f9e 100644 --- a/src/__tests__/Button.test.tsx +++ b/src/__tests__/Button.test.tsx @@ -10,9 +10,9 @@ expect.extend(toHaveNoViolations) describe('Button', () => { behavesAsComponent({Component: Button, options: {skipAs: true}}) - it('renders a ) - const button = await container.findByRole('button') + const button = container.getByRole('button') expect(button.textContent).toEqual('Default') }) @@ -23,76 +23,82 @@ describe('Button', () => { cleanup() }) - it('preserves "onClick" prop', async () => { + it('preserves "onClick" prop', () => { const onClick = jest.fn() const container = render() - const button = await container.findByRole('button') + const button = container.getByRole('button') fireEvent.click(button) expect(onClick).toHaveBeenCalledTimes(1) }) - it('respects width props', async () => { + it('respects width props', () => { const container = render() - const button = await container.findByRole('button') + const button = container.getByRole('button') expect(button).toHaveStyleRule('width', '200px') }) - it('respects the "disabled" prop', async () => { + it('respects the "disabled" prop', () => { const onClick = jest.fn() const container = render( ) - const button = await container.findByRole('button') + const button = container.getByRole('button') expect(button.hasAttribute('disabled')).toEqual(true) fireEvent.click(button) expect(onClick).toHaveBeenCalledTimes(0) }) - it('respects the "variant" prop', async () => { + it('respects the "variant" prop', () => { const container = render() - const button = await container.findByRole('button') + const button = container.getByRole('button') expect(button).toHaveStyleRule('font-size', '12px') }) - it('respects the "fontSize" prop over the "variant" prop', async () => { + it('respects the "fontSize" prop over the "variant" prop', () => { const container = render( ) - const button = await container.findByRole('button') + const button = container.getByRole('button') expect(button).toHaveStyleRule('font-size', '20px') }) - it('styles primary button appropriately', async () => { + it('styles primary button appropriately', () => { const container = render() - const button = await container.findByRole('button') + const button = container.getByRole('button') expect(button).toMatchSnapshot() }) - it('styles invisible button appropriately', async () => { + it('styles invisible button appropriately', () => { const container = render() - const button = await container.findByRole('button') + const button = container.getByRole('button') expect(button).toMatchSnapshot() }) - it('styles danger button appropriately', async () => { + it('styles danger button appropriately', () => { const container = render() - const button = await container.findByRole('button') + const button = container.getByRole('button') expect(button).toMatchSnapshot() }) - it('styles outline button appropriately', async () => { + it('styles outline button appropriately', () => { const container = render() - const button = await container.findByRole('button') + const button = container.getByRole('button') expect(button).toMatchSnapshot() }) - it('styles icon only button to make it a square', async () => { - const container = render() - const IconOnlyButton = await container.findByRole('button') + it('styles icon only button to make it a square', () => { + const container = render() + const IconOnlyButton = container.getByRole('button') expect(IconOnlyButton).toHaveStyleRule('padding-right', '8px') + expect(IconOnlyButton).toMatchSnapshot() + }) + it('makes sure icon button has an aria-label', () => { + const container = render() + const IconOnlyButton = container.getByLabelText('Search button') + expect(IconOnlyButton).toBeTruthy() }) }) diff --git a/src/__tests__/TextInput.test.tsx b/src/__tests__/TextInput.test.tsx index 09cc2439d77..a99c5eee014 100644 --- a/src/__tests__/TextInput.test.tsx +++ b/src/__tests__/TextInput.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import {TextInput} from '..' import {render, mount, behavesAsComponent, checkExports} from '../utils/testing' -import {render as HTMLRender, cleanup} from '@testing-library/react' +import {render as HTMLRender, cleanup, fireEvent} from '@testing-library/react' import {axe, toHaveNoViolations} from 'jest-axe' import 'babel-polyfill' import {SearchIcon} from '@primer/octicons-react' @@ -64,6 +64,72 @@ describe('TextInput', () => { expect(render()).toMatchSnapshot() }) + it('focuses the text input if you do not click the input element', () => { + const {container, getByLabelText} = HTMLRender( + <> + {/* eslint-disable-next-line jsx-a11y/label-has-for */} + + + + ) + + const icon = container.querySelector('svg')! + + expect(getByLabelText('Search')).not.toEqual(document.activeElement) + fireEvent.click(icon) + expect(getByLabelText('Search')).toEqual(document.activeElement) + }) + + it('renders with a loading indicator', () => { + expect( + render( + <> + + + + + + + + + + + + + + + + + + + + + + + + + ) + ).toMatchSnapshot() + }) + + it('indicates a busy status to assistive technology', () => { + const {container} = HTMLRender( + <> + {/* eslint-disable-next-line jsx-a11y/label-has-for */} + + + + ) + + expect(container.querySelector('span[aria-busy=true]')).not.toBeNull() + }) + it('should call onChange prop with input value', () => { const onChangeMock = jest.fn() const component = mount() diff --git a/src/__tests__/TextInputWithTokens.test.tsx b/src/__tests__/TextInputWithTokens.test.tsx index 79cf68a2bad..51cbfd28fc7 100644 --- a/src/__tests__/TextInputWithTokens.test.tsx +++ b/src/__tests__/TextInputWithTokens.test.tsx @@ -109,6 +109,95 @@ describe('TextInputWithTokens', () => { ).toMatchSnapshot() }) + it('renders with a loading indicator', () => { + const onRemoveMock = jest.fn() + expect( + render( + <> + + + + + + + + + + + + + + + + + + + + + + + + + ) + ).toMatchSnapshot() + }) + it('focuses the previous token when keying ArrowLeft', () => { const onRemoveMock = jest.fn() const {getByLabelText, getByText} = HTMLRender( diff --git a/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap b/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap index b7e6b825d1c..6a1ffc5e47e 100644 --- a/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap +++ b/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap @@ -104,6 +104,12 @@ exports[`ActionMenu renders consistently 1`] = ` margin-right: -4px; } +@media (forced-colors:active) { + .c1:focus { + outline: solid 1px transparent; + } +} +
@@ -198,6 +204,12 @@ exports[`Button styles danger button appropriately 1`] = ` border-color: btn.danger.selectedBorder; } +@media (forced-colors:active) { + .c0:focus { + outline: solid 1px transparent; + } +} + `; +exports[`Button styles icon only button to make it a square 1`] = ` +.c1 { + display: inline-block; +} + +.c0 { + border-radius: 2; + border: 1px solid; + font-family: inherit; + font-weight: bold; + line-height: 20px; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-text-decoration: none; + text-decoration: none; + text-align: center; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 8px; + padding-right: 8px; + font-size: 14px; + color: btn.text; + background-color: btn.bg; + box-shadow: undefined,undefined; +} + +.c0:focus { + outline: none; +} + +.c0:disabled { + cursor: default; + color: primer.fg.disabled; +} + +.c0:disabled [data-component=ButtonCounter] { + color: inherit; +} + +.c0:disabled svg { + opacity: 0.6; +} + +.c0 [data-component=ButtonCounter] { + font-size: 14px; +} + +.c0:hover:not([disabled]) { + background-color: btn.hoverBg; +} + +.c0:focus:not([disabled]) { + box-shadow: undefined; +} + +.c0:active:not([disabled]) { + background-color: btn.activeBg; + border-color: btn.activeBorder; +} + +.c0[aria-expanded=true] { + background-color: btn.activeBg; + border-color: btn.activeBorder; +} + +@media (forced-colors:active) { + .c0:focus { + outline: solid 1px transparent; + } +} + + +`; + exports[`Button styles invisible button appropriately 1`] = ` .c0 { border-radius: 2; @@ -294,6 +411,12 @@ exports[`Button styles invisible button appropriately 1`] = ` background-color: btn.selectedBg; } +@media (forced-colors:active) { + .c0:focus { + outline: solid 1px transparent; + } +} + + + +

No visual

+ + + + + + + + + + +

Leading visual

+ + + + + + + + + + +

Trailing visual

+ + + + + + + + + + +

Both visuals

+ + + + + + + + + + + ) +} + +WithLoadingIndicator.parameters = {controls: {exclude: ['loading']}} + export const ContrastTextInput = (args: TextInputProps) => { const [value, setValue] = useState('') diff --git a/src/stories/TextInputWithTokens.stories.tsx b/src/stories/TextInputWithTokens.stories.tsx index b02db775a99..084d3179f90 100644 --- a/src/stories/TextInputWithTokens.stories.tsx +++ b/src/stories/TextInputWithTokens.stories.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useState} from 'react' import {Meta} from '@storybook/react' import {CheckIcon, NumberIcon} from '@primer/octicons-react' -import {BaseStyles, Box, ThemeProvider} from '..' +import {BaseStyles, Box, FormControl, ThemeProvider} from '..' import TextInputWithTokens, {TextInputWithTokensProps} from '../TextInputWithTokens' import IssueLabelToken from '../Token/IssueLabelToken' @@ -47,6 +47,21 @@ export default { type: 'boolean' } }, + loading: { + name: 'loading', + defaultValue: false, + control: { + type: 'boolean' + } + }, + loaderPosition: { + name: 'loaderPosition', + defaultValue: 'auto', + options: ['auto', 'leading', 'trailing'], + control: { + type: 'radio' + } + }, size: { name: 'size (token size)', defaultValue: 'extralarge', @@ -121,6 +136,59 @@ export const WithTrailingVisual = (args: TextInputWithTokensProps) => { WithTrailingVisual.parameters = {controls: {exclude: [excludedControls, 'maxHeight']}} +export const WithLoadingIndicator = (args: TextInputWithTokensProps) => { + const [tokens, setTokens] = useState([...mockTokens].slice(0, 3)) + const [loading, setLoading] = useState(true) + const onTokenRemove: (tokenId: string | number) => void = tokenId => { + setTokens(tokens.filter(token => token.id !== tokenId)) + } + const toggleLoadingState = () => { + setLoading(!loading) + } + + return ( +
+ + + + + + + No visual + + + + + Leading visual + + + + + Both visuals + + + +
+ ) +} + +WithLoadingIndicator.parameters = {controls: {exclude: [excludedControls, 'maxHeight', 'loading']}} + export const UsingIssueLabelTokens = (args: TextInputWithTokensProps) => { const [tokens, setTokens] = useState([ {text: 'enhancement', id: 1, fillColor: '#a2eeef'}, diff --git a/src/stories/Tooltip.stories.tsx b/src/stories/Tooltip.stories.tsx new file mode 100644 index 00000000000..59d034282f1 --- /dev/null +++ b/src/stories/Tooltip.stories.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import {Meta} from '@storybook/react' +import {BaseStyles, ThemeProvider, IconButton} from '..' +import Box from '../Box' +import Tooltip from '../Tooltip' +import {SearchIcon} from '@primer/octicons-react' + +export default { + title: 'Tooltip/Default', + component: Tooltip, + + decorators: [ + Story => { + return ( + + + + + + ) + } + ] +} as Meta + +export const TextTooltip = () => ( + + Text with a tooltip + +) + +export const IconButtonTooltip = () => ( + + + + + +)