Skip to content

Commit

Permalink
Merge branch 'main' into VanAnderson/action-menu-on-confirm
Browse files Browse the repository at this point in the history
  • Loading branch information
VanAnderson authored May 18, 2021
2 parents 2ec6c7b + d2e341c commit 2d99788
Show file tree
Hide file tree
Showing 28 changed files with 242 additions and 39 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-days-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/components": minor
---

New `Spinner` Component
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
},
// rules which apply only to TS
{
"parserOptions": {
"project": "tsconfig.json"
},
"files": [
"**/*.ts",
"**/*.tsx"
Expand All @@ -88,6 +91,7 @@
],
"rules": {
"@typescript-eslint/no-explicit-any": 2,
"@typescript-eslint/no-unnecessary-condition": 2,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-unused-vars": [
"error",
Expand Down
38 changes: 38 additions & 0 deletions docs/content/Spinner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: Spinner
status: alpha
---

Use Spinner to let users know that content is being loaded.

## Examples

### Default (Medium)

```jsx live
<Spinner />
```

### Small

```jsx live
<Spinner size="small"/>
```

### Large

```jsx live
<Spinner size="large"/>
```

## System props

Spinner components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.

## Component props

StyledOcticon passes all of its props except the common system props down to the [Octicon component](https://github.com/primer/octicons/tree/master/lib/octicons_react#usage), including:

| Name | Type | Default | Description |
| :--- | :------------------------------------- | :------: | :------------------------------------------------------- |
| size | 'small' &#124; 'medium' &#124; 'large' | 'medium' | Sets the uniform `width` and `height` of the SVG element |
2 changes: 2 additions & 0 deletions docs/src/@primer/gatsby-theme-doctocat/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@
url: /SelectMenu
- title: SideNav
url: /SideNav
- title: Spinner
url: /Spinner
- title: StateLabel
url: /StateLabel
- title: StyledOcticon
Expand Down
7 changes: 4 additions & 3 deletions src/ActionList/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export function List(props: ListProps): JSX.Element {
* or the default `Item` renderer.
*/
const renderItem = (itemProps: ItemInput, item: ItemInput) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const ItemComponent = ('renderItem' in itemProps && itemProps.renderItem) || props.renderItem || Item
const key = itemProps.key ?? itemProps.id?.toString() ?? uniqueId()
return (
Expand All @@ -176,7 +177,7 @@ export function List(props: ListProps): JSX.Element {

if (!isGroupedListProps(props)) {
// When no `groupMetadata`s is provided, collect rendered `Item`s into a single anonymous `Group`.
groups = [{items: props.items?.map(item => renderItem(item, item)), groupId: singleGroupId}]
groups = [{items: props.items.map(item => renderItem(item, item)), groupId: singleGroupId}]
} else {
// When `groupMetadata` is provided, collect rendered `Item`s into their associated `Group`s.

Expand Down Expand Up @@ -214,7 +215,7 @@ export function List(props: ListProps): JSX.Element {

return (
<StyledList {...props}>
{groups?.map(({header, ...groupProps}, index) => {
{groups.map(({header, ...groupProps}, index) => {
const hasFilledHeader = header?.variant === 'filled'
const shouldShowDivider = index > 0 && !hasFilledHeader
return (
Expand All @@ -229,7 +230,7 @@ export function List(props: ListProps): JSX.Element {
...(header && {
header: {
...header,
sx: {...headerStyle, ...header?.sx}
sx: {...headerStyle, ...header.sx}
}
}),
...groupProps
Expand Down
6 changes: 3 additions & 3 deletions src/Caret.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ const perpendicularEdge = {
left: 'Top'
}

function getEdgeAlign(location: Location): Alignment[] {
function getEdgeAlign(location: Location) {
const [edge, align] = location.split('-')
return [edge as Alignment, align as Alignment]
return [edge as Alignment, align as Alignment | undefined] as const
}

function getPosition(edge: Alignment, align: Alignment, spacing: number) {
function getPosition(edge: Alignment, align: Alignment | undefined, spacing: number) {
const opposite = oppositeEdge[edge].toLowerCase()
const perp = perpendicularEdge[edge].toLowerCase()
return {
Expand Down
12 changes: 4 additions & 8 deletions src/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useDialog from './hooks/useDialog'
import sx, {SxProp} from './sx'
import Text from './Text'
import {ComponentProps} from './utils/types'
import {useCombinedRefs} from './hooks/useCombinedRefs'

const noop = () => null

Expand Down Expand Up @@ -78,7 +79,6 @@ const Overlay = styled.span`
right: 0;
bottom: 0;
left: 0;
z-index: 80;
display: block;
cursor: default;
content: ' ';
Expand All @@ -95,14 +95,10 @@ type InternalDialogProps = {
returnFocusRef?: React.RefObject<HTMLElement>
} & ComponentProps<typeof DialogBase>

const Dialog = forwardRef<HTMLElement, InternalDialogProps>(
(
{children, onDismiss = noop, isOpen, initialFocusRef, returnFocusRef, ...props},
forwardedRef: React.ForwardedRef<HTMLElement>
) => {
const backupRef = useRef(null)
const Dialog = forwardRef<HTMLDivElement, InternalDialogProps>(
({children, onDismiss = noop, isOpen, initialFocusRef, returnFocusRef, ...props}, forwardedRef) => {
const overlayRef = useRef(null)
const modalRef = (forwardedRef as React.RefObject<HTMLDivElement>) ?? backupRef
const modalRef = useCombinedRefs(forwardedRef)
const closeButtonRef = useRef(null)

const onCloseClick = () => {
Expand Down
8 changes: 3 additions & 5 deletions src/Portal/Portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {createPortal} from 'react-dom'
const PRIMER_PORTAL_ROOT_ID = '__primerPortalRoot__'
const DEFAULT_PORTAL_CONTAINER_NAME = '__default__'

const portalRootRegistry: {[key: string]: Element} = {}
const portalRootRegistry: Partial<Record<string, Element>> = {}

/**
* Register a container to serve as a portal root.
Expand All @@ -20,10 +20,8 @@ export function registerPortalRoot(root: Element, name = DEFAULT_PORTAL_CONTAINE
// with id __primerPortalRoot__, allow that element to serve as the default portal root.
// Otherwise, create that element and attach it to the end of document.body.
function ensureDefaultPortal() {
if (
!(DEFAULT_PORTAL_CONTAINER_NAME in portalRootRegistry) ||
!document.body.contains(portalRootRegistry[DEFAULT_PORTAL_CONTAINER_NAME])
) {
const existingDefaultPortalContainer = portalRootRegistry[DEFAULT_PORTAL_CONTAINER_NAME]
if (!existingDefaultPortalContainer || !document.body.contains(existingDefaultPortalContainer)) {
let defaultPortalContainer = document.getElementById(PRIMER_PORTAL_ROOT_ID)
if (!(defaultPortalContainer instanceof Element)) {
defaultPortalContainer = document.createElement('div')
Expand Down
58 changes: 58 additions & 0 deletions src/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react'
import styled from 'styled-components'
import {COMMON, SystemCommonProps} from './constants'
import sx, {SxProp} from './sx'
import {ComponentProps} from './utils/types'

const sizeMap = {
small: '16px',
medium: '32px',
large: '64px'
}

export interface SpinnerInternalProps {
size?: keyof typeof sizeMap
}

function Spinner({size: sizeKey, ...props}: SpinnerInternalProps) {
const size = (sizeKey && sizeMap[sizeKey]) ?? sizeMap.medium

return (
<svg height={size} width={size} viewBox="0 0 16 16" fill="none" {...props}>
<circle
cx="8"
cy="8"
r="7"
stroke="currentColor"
strokeOpacity="0.25"
strokeWidth="2"
vectorEffect="non-scaling-stroke"
/>
<path
d="M15 8a7.002 7.002 0 00-7-7"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
</svg>
)
}

const StyledSpinner = styled(Spinner)<SystemCommonProps & SxProp>`
@keyframes rotate-keyframes {
100% {
transform: rotate(360deg);
}
}
animation: rotate-keyframes 1s linear infinite;
${COMMON}
${sx}
`

StyledSpinner.displayName = 'Spinner'

export type SpinnerProps = ComponentProps<typeof StyledSpinner>
export default StyledSpinner
1 change: 1 addition & 0 deletions src/StateLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ function StateLabel({children, status, variant: variantProp, ...rest}: StateLabe
const octiconProps = variantProp === 'small' ? {width: '1em'} : {}
return (
<StateLabelBase {...rest} variant={variantProp} status={status}>
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{status && <StyledOcticon mr={1} {...octiconProps} icon={octiconMap[status] || QuestionIcon} />}
{children}
</StateLabelBase>
Expand Down
6 changes: 5 additions & 1 deletion src/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function useTheme() {
return React.useContext(ThemeContext)
}

export function useColorSchemeVar(values: Record<string, string>, fallback?: string) {
export function useColorSchemeVar(values: Partial<Record<string, string>>, fallback?: string) {
const {colorScheme = ''} = useTheme()
return values[colorScheme] ?? fallback
}
Expand All @@ -99,16 +99,19 @@ function useSystemColorMode() {
const [systemColorMode, setSystemColorMode] = React.useState(getSystemColorMode)

React.useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const media = window?.matchMedia?.('(prefers-color-scheme: dark)')

function handleChange(event: MediaQueryListEvent) {
const isNight = event.matches
setSystemColorMode(isNight ? 'night' : 'day')
}

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
media?.addEventListener('change', handleChange)

return function cleanup() {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
media?.removeEventListener('change', handleChange)
}
}, [])
Expand All @@ -117,6 +120,7 @@ function useSystemColorMode() {
}

function getSystemColorMode(): ColorMode {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)')?.matches) {
return 'night'
}
Expand Down
3 changes: 3 additions & 0 deletions src/__tests__/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"extends": [
"plugin:jest/recommended"
],
"parserOptions": {
"project": "../../tsconfig.json"
},
"rules": {
"@typescript-eslint/no-non-null-assertion": 0
}
Expand Down
7 changes: 6 additions & 1 deletion src/__tests__/ActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ function SimpleActionList(): JSX.Element {
}

describe('ActionList', () => {
behavesAsComponent({Component: ActionList, systemPropArray: [COMMON], options: {skipAs: true, skipSx: true}})
behavesAsComponent({
Component: ActionList,
systemPropArray: [COMMON],
options: {skipAs: true, skipSx: true},
toRender: () => <ActionList items={[]} />
})

checkExports('ActionList', {
default: undefined,
Expand Down
4 changes: 3 additions & 1 deletion src/__tests__/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ describe('ActionMenu', () => {
expect(portalRoot).toBeTruthy()
const itemText = items
.map((i: ItemProps) => {
if (i.hasOwnProperty('text')) return i?.text
if (i.hasOwnProperty('text')) {
return i.text
}
})
.join('')
expect(portalRoot?.textContent?.trim()).toEqual(itemText)
Expand Down
6 changes: 4 additions & 2 deletions src/__tests__/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ describe('DropdownMenu', () => {
expect(portalRoot).toBeTruthy()
const itemText = items
.map((i: ItemInput) => {
if (i.hasOwnProperty('text')) return i?.text
if (i.hasOwnProperty('text')) {
return i.text
}
})
.join('')
expect(portalRoot?.textContent?.trim()).toEqual(itemText)
Expand Down Expand Up @@ -107,7 +109,7 @@ describe('DropdownMenu', () => {
act(() => {
fireEvent.click(menuItem as Element)
})
expect(anchor?.textContent).toEqual('Baz')
expect(anchor.textContent).toEqual('Baz')
})

it('should dismiss the overlay on clicking outside overlay', async () => {
Expand Down
44 changes: 44 additions & 0 deletions src/__tests__/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react'
import {Spinner, SpinnerProps} from '..'
import {behavesAsComponent, checkExports} from '../utils/testing'
import {COMMON} from '../constants'
import {render as HTMLRender, cleanup} from '@testing-library/react'
import {axe, toHaveNoViolations} from 'jest-axe'
import 'babel-polyfill'
expect.extend(toHaveNoViolations)

describe('Spinner', () => {
afterEach(() => {
cleanup()
})

behavesAsComponent({
Component: Spinner,
systemPropArray: [COMMON]
})

checkExports('Spinner', {
default: Spinner
})

it('should have no axe violations', async () => {
const {container} = HTMLRender(<Spinner />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})

it('should respect size arguments', () => {
const expectSize = (input: SpinnerProps['size'] | undefined, expectedSize: string) => {
const {container} = HTMLRender(<Spinner size={input} />)
const svg = container.querySelector('svg')!
expect(svg.getAttribute('height')).toEqual(expectedSize)
expect(svg.getAttribute('width')).toEqual(expectedSize)
}

// default: medium
expectSize(undefined, '32px')
expectSize('small', '16px')
expectSize('medium', '32px')
expectSize('large', '64px')
})
})
1 change: 0 additions & 1 deletion src/__tests__/__snapshots__/Dialog.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ Array [
right: 0;
bottom: 0;
left: 0;
z-index: 80;
display: block;
cursor: default;
content: ' ';
Expand Down
Loading

0 comments on commit 2d99788

Please sign in to comment.