From da4c45f5614233d698bba3445818f2bd1f2c634c Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Wed, 24 Feb 2021 10:15:47 -0500 Subject: [PATCH 01/31] feat: Add 'ActionList' --- src/ActionList/ActionList.tsx | 9 +++++++++ src/ActionList/ActionListSection.tsx | 9 +++++++++ src/ActionList/ActionListSectionDivider.tsx | 9 +++++++++ src/ActionList/ActionListSectionHeader.tsx | 9 +++++++++ src/ActionList/index.ts | 4 ++++ src/stories/ActionList.stories.tsx | 21 +++++++++++++++++++++ 6 files changed, 61 insertions(+) create mode 100644 src/ActionList/ActionList.tsx create mode 100644 src/ActionList/ActionListSection.tsx create mode 100644 src/ActionList/ActionListSectionDivider.tsx create mode 100644 src/ActionList/ActionListSectionHeader.tsx create mode 100644 src/ActionList/index.ts create mode 100644 src/stories/ActionList.stories.tsx diff --git a/src/ActionList/ActionList.tsx b/src/ActionList/ActionList.tsx new file mode 100644 index 00000000000..9beda95f4cc --- /dev/null +++ b/src/ActionList/ActionList.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export interface ActionListProps extends React.ComponentPropsWithoutRef<'div'> { + [key: string]: unknown +} + +export function ActionList(props: ActionListProps): JSX.Element { + return
+} diff --git a/src/ActionList/ActionListSection.tsx b/src/ActionList/ActionListSection.tsx new file mode 100644 index 00000000000..f39304e05a4 --- /dev/null +++ b/src/ActionList/ActionListSection.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export interface ActionListSectionProps extends React.ComponentPropsWithoutRef<'div'> { + [key: string]: unknown +} + +export function ActionListSection(props: ActionListSectionProps): JSX.Element { + return
+} diff --git a/src/ActionList/ActionListSectionDivider.tsx b/src/ActionList/ActionListSectionDivider.tsx new file mode 100644 index 00000000000..75dab2e3c1e --- /dev/null +++ b/src/ActionList/ActionListSectionDivider.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export interface ActionListSectionDividerProps extends React.ComponentPropsWithoutRef<'div'> { + [key: string]: unknown +} + +export function ActionListSectionDivider(props: ActionListSectionDividerProps): JSX.Element { + return
+} diff --git a/src/ActionList/ActionListSectionHeader.tsx b/src/ActionList/ActionListSectionHeader.tsx new file mode 100644 index 00000000000..5ada3c6750f --- /dev/null +++ b/src/ActionList/ActionListSectionHeader.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export interface ActionListSectionHeaderProps extends React.ComponentPropsWithoutRef<'div'> { + [key: string]: unknown +} + +export function ActionListSectionHeader(props: ActionListSectionHeaderProps): JSX.Element { + return
+} diff --git a/src/ActionList/index.ts b/src/ActionList/index.ts new file mode 100644 index 00000000000..391fb899902 --- /dev/null +++ b/src/ActionList/index.ts @@ -0,0 +1,4 @@ +export * from './ActionList' +export * from './ActionListSection' +export * from './ActionListSectionDivider' +export * from './ActionListSectionHeader' diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx new file mode 100644 index 00000000000..badc6cf05dd --- /dev/null +++ b/src/stories/ActionList.stories.tsx @@ -0,0 +1,21 @@ +import {Meta} from '@storybook/react' +import React from 'react' +import {ActionList} from '../ActionList' +import BaseStyles from '../BaseStyles' + +const meta: Meta = { + title: 'Composite components/ActionList', + component: ActionList, + decorators: [ + (Story: React.ComponentType): JSX.Element => ( + + + + ) + ] +} +export default meta + +export function ActionListStory(): JSX.Element { + return Hello, world! +} From 2b33f5f1bac26b3a79531f53937ede031d89f04d Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Wed, 24 Feb 2021 12:02:31 -0500 Subject: [PATCH 02/31] feat: Style 'ActionListSectionHeader' --- .eslintrc.json | 5 ++- src/ActionList/ActionListSectionHeader.tsx | 49 ++++++++++++++++++++-- src/ActionList/StyledDiv.tsx | 7 ++++ src/ActionList/variables.ts | 20 +++++++++ src/stories/ActionList.stories.tsx | 14 ++++++- 5 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/ActionList/StyledDiv.tsx create mode 100644 src/ActionList/variables.ts diff --git a/.eslintrc.json b/.eslintrc.json index 65235903158..96c00f8ee14 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -38,7 +38,10 @@ // rules which apply only to TS { "files": ["**/*.ts", "**/*.tsx"], - "extends": ["plugin:@typescript-eslint/recommended"] + "extends": ["plugin:@typescript-eslint/recommended"], + "rules": { + "@typescript-eslint/no-unused-vars": ["warn", {"argsIgnorePattern": "^_"}] + } } ] } diff --git a/src/ActionList/ActionListSectionHeader.tsx b/src/ActionList/ActionListSectionHeader.tsx index 5ada3c6750f..e044fb16e0a 100644 --- a/src/ActionList/ActionListSectionHeader.tsx +++ b/src/ActionList/ActionListSectionHeader.tsx @@ -1,9 +1,52 @@ import React from 'react' +import {StyledDiv} from './StyledDiv' +import {s8, s20, s32, gray100, gray200, textSecondary} from './variables' +import type {SystemStyleObject} from '@styled-system/css' export interface ActionListSectionHeaderProps extends React.ComponentPropsWithoutRef<'div'> { - [key: string]: unknown + variant: 'subtle' | 'filled' + title: string + auxiliaryText?: string } -export function ActionListSectionHeader(props: ActionListSectionHeaderProps): JSX.Element { - return
+/** Styles used by all variants. */ +const sharedStyles: SystemStyleObject = { + padding: `${(s32 - s20) / 2}px ${s8}px`, + fontSize: '12px', + fontWeight: 'bold', + color: textSecondary +} + +/** Styles used by the 'filled' variant. */ +const filledVariantStyles: SystemStyleObject = { + background: gray100, + margin: `${s8}px -${s8}px`, + borderTop: `1px solid ${gray200}`, + borderBottom: `1px solid ${gray200}`, + + '&:first-child': { + marginTop: 0 + } +} + +export function ActionListSectionHeader({ + variant, + title, + auxiliaryText, + children: _children, + ...props +}: ActionListSectionHeaderProps): JSX.Element { + return ( + + {title} + {auxiliaryText && auxiliaryText} + + ) } diff --git a/src/ActionList/StyledDiv.tsx b/src/ActionList/StyledDiv.tsx new file mode 100644 index 00000000000..497d6b7cf2f --- /dev/null +++ b/src/ActionList/StyledDiv.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import styled from 'styled-components' +import sx, {SxProp} from '../sx' + +export const StyledDiv = styled('div') & SxProp>` + ${sx} +` diff --git a/src/ActionList/variables.ts b/src/ActionList/variables.ts new file mode 100644 index 00000000000..9e78d67b8a8 --- /dev/null +++ b/src/ActionList/variables.ts @@ -0,0 +1,20 @@ +// expanded from src/support/variables/layout.scss +// adds 12px to the scale as of https://github.com/github/design-systems/issues/992 +// +// https://github.com/github/design-systems/issues/1272 + +export const s8 = 8 +export const s20 = 20 +export const s32 = 32 + +// pulled from src/support/variables/color-system.scss + +// -------- Grays -------- + +export const gray100 = '#f6f8fa' +export const gray200 = '#e1e4e8' +export const gray500 = '#6a737d' + +// Typographic colors, from some color mode file + +export const textSecondary = gray500 diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index badc6cf05dd..7d4bb300714 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -1,6 +1,6 @@ import {Meta} from '@storybook/react' import React from 'react' -import {ActionList} from '../ActionList' +import {ActionList, ActionListSectionHeader} from '../ActionList' import BaseStyles from '../BaseStyles' const meta: Meta = { @@ -19,3 +19,15 @@ export default meta export function ActionListStory(): JSX.Element { return Hello, world! } + +export function ActionListSectionHeaderStory(): JSX.Element { + return ( + <> +

ActionListSectionHeader

+

Filled Variant

+ +

Subtle Variant

+ + + ) +} From a3ba467d634b7116d4dd21ec5b4f641f98459a9b Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Fri, 26 Feb 2021 11:28:48 -0500 Subject: [PATCH 03/31] feat: Increase 'Action List Story' fidelity --- src/ActionList/ActionListItem.tsx | 58 ++++++++++++++++++++++ src/ActionList/ActionListSectionHeader.tsx | 5 +- src/ActionList/index.ts | 1 + src/ActionList/variables.ts | 4 ++ src/stories/ActionList.stories.tsx | 46 +++++++++++++++-- 5 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 src/ActionList/ActionListItem.tsx diff --git a/src/ActionList/ActionListItem.tsx b/src/ActionList/ActionListItem.tsx new file mode 100644 index 00000000000..67606ecbd97 --- /dev/null +++ b/src/ActionList/ActionListItem.tsx @@ -0,0 +1,58 @@ +import type {IconProps} from '@primer/octicons-react' +import type {SystemStyleObject} from '@styled-system/css' +import React from 'react' +import {StyledDiv} from './StyledDiv' +import {borderRadius2} from './variables' + +export interface ActionListItemProps extends React.ComponentPropsWithoutRef<'div'> { + text: string + description?: string + descriptionVariant?: 'inline' | 'block' + leadingVisual?: React.FunctionComponent + leadingVisualSize?: 16 | 20 + size?: 'small' | 'medium' | 'large' + variant?: 'default' | 'singleSelection' | 'multiSelection' | 'danger' | 'static' +} + +const baseStyles: SystemStyleObject = { + position: 'relative', + display: 'flex', + alignItems: 'start', + borderRadius: borderRadius2, + fontWeight: 'normal', + color: 'inherit', + textDecoration: 'none', + border: 'none', + background: 'none', + textAlign: 'start', + margin: 0 +} + +const inlineDescriptionStyles: SystemStyleObject = { + flexDirection: 'row' +} + +const blockDescriptionStyles: SystemStyleObject = { + flexDirection: 'column' +} + +export function ActionListItem({ + text, + description, + descriptionVariant = 'inline', + size: _size = 'small', + ...props +}: ActionListItemProps): JSX.Element { + return ( + +
{text}
+ {description &&
{description}
} +
+ ) +} diff --git a/src/ActionList/ActionListSectionHeader.tsx b/src/ActionList/ActionListSectionHeader.tsx index e044fb16e0a..8f8cd976252 100644 --- a/src/ActionList/ActionListSectionHeader.tsx +++ b/src/ActionList/ActionListSectionHeader.tsx @@ -9,8 +9,7 @@ export interface ActionListSectionHeaderProps extends React.ComponentPropsWithou auxiliaryText?: string } -/** Styles used by all variants. */ -const sharedStyles: SystemStyleObject = { +const baseStyles: SystemStyleObject = { padding: `${(s32 - s20) / 2}px ${s8}px`, fontSize: '12px', fontWeight: 'bold', @@ -40,7 +39,7 @@ export function ActionListSectionHeader({ Hello, world! + return ( + <> +

ActionListSectionHeader

+

Filled Variant

+ + + + + + + + +
repo:github/memex,github/github
+
labels:Flux,Q2
+
+
+ + + + + + + + + + + + +
+ + ) } export function ActionListSectionHeaderStory(): JSX.Element { @@ -25,9 +63,9 @@ export function ActionListSectionHeaderStory(): JSX.Element { <>

ActionListSectionHeader

Filled Variant

- +

Subtle Variant

- + ) } From 445215fd59c5c1b53a5e46593cfa223f2d5b7df2 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Thu, 4 Mar 2021 23:15:39 -0500 Subject: [PATCH 04/31] chore: Split ActionList Storybook --- src/ActionList/ActionList.tsx | 8 +- src/ActionList/ActionListItem.tsx | 95 ++++++++++---- src/ActionList/ActionListSection.tsx | 2 +- src/ActionList/ActionListSectionDivider.tsx | 25 +++- src/ActionList/ActionListSectionHeader.tsx | 1 + src/ActionList/StyledSpan.tsx | 7 + src/ActionList/variables.ts | 12 ++ src/stories/ActionList.stories.tsx | 138 ++++++++++++++------ 8 files changed, 217 insertions(+), 71 deletions(-) create mode 100644 src/ActionList/StyledSpan.tsx diff --git a/src/ActionList/ActionList.tsx b/src/ActionList/ActionList.tsx index 9beda95f4cc..e667b973dc9 100644 --- a/src/ActionList/ActionList.tsx +++ b/src/ActionList/ActionList.tsx @@ -1,9 +1,15 @@ +import type {SystemStyleObject} from '@styled-system/css' import React from 'react' +import {StyledDiv} from './StyledDiv' export interface ActionListProps extends React.ComponentPropsWithoutRef<'div'> { [key: string]: unknown } +const actionListStyles: SystemStyleObject = { + fontSize: '14px' +} + export function ActionList(props: ActionListProps): JSX.Element { - return
+ return } diff --git a/src/ActionList/ActionListItem.tsx b/src/ActionList/ActionListItem.tsx index 67606ecbd97..2415d0d426c 100644 --- a/src/ActionList/ActionListItem.tsx +++ b/src/ActionList/ActionListItem.tsx @@ -2,7 +2,19 @@ import type {IconProps} from '@primer/octicons-react' import type {SystemStyleObject} from '@styled-system/css' import React from 'react' import {StyledDiv} from './StyledDiv' -import {borderRadius2} from './variables' +import {StyledSpan} from './StyledSpan' +import { + borderRadius2, + s6, + s8, + s16, + s20, + actionListItemHoverBg, + actionListItemDangerHoverBg, + textSecondary, + textDanger, + textSmall +} from './variables' export interface ActionListItemProps extends React.ComponentPropsWithoutRef<'div'> { text: string @@ -14,45 +26,76 @@ export interface ActionListItemProps extends React.ComponentPropsWithoutRef<'div variant?: 'default' | 'singleSelection' | 'multiSelection' | 'danger' | 'static' } -const baseStyles: SystemStyleObject = { - position: 'relative', - display: 'flex', - alignItems: 'start', - borderRadius: borderRadius2, - fontWeight: 'normal', - color: 'inherit', - textDecoration: 'none', - border: 'none', - background: 'none', - textAlign: 'start', - margin: 0 +function baseStyles({variant}: {variant: ActionListItemProps['variant']}): SystemStyleObject { + return { + position: 'relative', + display: 'flex', + alignItems: 'start', + borderRadius: borderRadius2, + fontWeight: 'normal', + color: variant === 'danger' ? textDanger : 'inherit', + textDecoration: 'none', + border: 'none', + background: 'none', + textAlign: 'start', + margin: 0, + padding: `${s6}px ${s8}px`, + + '@media (hover: hover) and (pointer: fine)': { + ':hover': { + background: variant === 'danger' ? actionListItemDangerHoverBg : actionListItemHoverBg, + cursor: 'pointer' + } + } + } +} + +function textContainerStyles({ + descriptionVariant +}: { + descriptionVariant: ActionListItemProps['descriptionVariant'] +}): SystemStyleObject { + return {flexDirection: descriptionVariant === 'inline' ? 'row' : 'column'} } -const inlineDescriptionStyles: SystemStyleObject = { - flexDirection: 'row' +const leadingVisualStyles: SystemStyleObject = { + width: `${s16}px`, + height: `${s20}px`, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + marginRight: `${s8}px`, + + svg: { + fill: textSecondary, + fontSize: textSmall + } } -const blockDescriptionStyles: SystemStyleObject = { - flexDirection: 'column' +const descriptionStyles: SystemStyleObject = { + color: textSecondary } export function ActionListItem({ text, description, descriptionVariant = 'inline', + leadingVisual: LeadingVisual, size: _size = 'small', + variant = 'default', ...props }: ActionListItemProps): JSX.Element { return ( - -
{text}
- {description &&
{description}
} + + {LeadingVisual && ( + + + + )} + +
{text}
+ {description && {description}} +
) } diff --git a/src/ActionList/ActionListSection.tsx b/src/ActionList/ActionListSection.tsx index f39304e05a4..6effe9e5b90 100644 --- a/src/ActionList/ActionListSection.tsx +++ b/src/ActionList/ActionListSection.tsx @@ -5,5 +5,5 @@ export interface ActionListSectionProps extends React.ComponentPropsWithoutRef<' } export function ActionListSection(props: ActionListSectionProps): JSX.Element { - return
+ return
} diff --git a/src/ActionList/ActionListSectionDivider.tsx b/src/ActionList/ActionListSectionDivider.tsx index 75dab2e3c1e..a88eefa6d70 100644 --- a/src/ActionList/ActionListSectionDivider.tsx +++ b/src/ActionList/ActionListSectionDivider.tsx @@ -1,9 +1,32 @@ import React from 'react' +import type {SystemStyleObject} from '@styled-system/css' +import {actionListItemDivider, s16, s8} from './variables' +import {StyledDiv} from './StyledDiv' export interface ActionListSectionDividerProps extends React.ComponentPropsWithoutRef<'div'> { [key: string]: unknown } +const actionListSectionDividerStyles: SystemStyleObject = { + position: 'relative', + height: '1px', + background: actionListItemDivider, + border: 0, + margin: `${s8 - 1}px ${-s8}px ${s8}px`, + padding: 0, + + '::before': { + content: '""', + display: 'block', + position: 'absolute', + width: `calc(100% - ${s16}px)`, + height: '1px', + background: actionListItemDivider, + top: 0, + left: `${s8}px` + } +} + export function ActionListSectionDivider(props: ActionListSectionDividerProps): JSX.Element { - return
+ return } diff --git a/src/ActionList/ActionListSectionHeader.tsx b/src/ActionList/ActionListSectionHeader.tsx index 8f8cd976252..c725ef2f4f5 100644 --- a/src/ActionList/ActionListSectionHeader.tsx +++ b/src/ActionList/ActionListSectionHeader.tsx @@ -37,6 +37,7 @@ export function ActionListSectionHeader({ }: ActionListSectionHeaderProps): JSX.Element { return ( & SxProp>` + ${sx} +` diff --git a/src/ActionList/variables.ts b/src/ActionList/variables.ts index a5dbb3f2242..496b1148d97 100644 --- a/src/ActionList/variables.ts +++ b/src/ActionList/variables.ts @@ -1,3 +1,10 @@ +// Most of this file should be provided by primer/src/support already + +export const actionListItemHoverBg = '#F0F3F5' +export const actionListItemDivider = '#E9E9ED' + +export const actionListItemDangerHoverBg = '#FFF0F2' + // expanded from src/support/variables/misc.scss export const borderRadius2 = '6px' @@ -7,7 +14,9 @@ export const borderRadius2 = '6px' // // https://github.com/github/design-systems/issues/1272 +export const s6 = 6 export const s8 = 8 +export const s16 = 16 export const s20 = 20 export const s32 = 32 @@ -22,3 +31,6 @@ export const gray500 = '#6a737d' // Typographic colors, from some color mode file export const textSecondary = gray500 +export const textDanger = '#CB2431' + +export const textSmall = '12px' diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index 24981ca9a7c..9a6d6696873 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -1,6 +1,25 @@ +import { + TypographyIcon, + VersionsIcon, + SearchIcon, + NoteIcon, + ProjectIcon, + FilterIcon, + GearIcon +} from '@primer/octicons-react' import {Meta} from '@storybook/react' +import type {SystemStyleObject} from '@styled-system/css' import React from 'react' -import {ActionList, ActionListItem, ActionListSection, ActionListSectionHeader} from '../ActionList' +import {ThemeProvider} from 'styled-components' +import {theme} from '..' +import { + ActionList, + ActionListItem, + ActionListSection, + ActionListSectionDivider, + ActionListSectionHeader +} from '../ActionList' +import {StyledDiv} from '../ActionList/StyledDiv' import BaseStyles from '../BaseStyles' const meta: Meta = { @@ -8,60 +27,95 @@ const meta: Meta = { component: ActionList, decorators: [ (Story: React.ComponentType): JSX.Element => ( - - - + + + + + ) ] } export default meta -export function ActionListStory(): JSX.Element { +const ersatzOverlayStyles: SystemStyleObject = { + borderRadius: '12px', + boxShadow: '0 1px 3px rgba(0,0,0,.12),0 8px 24px rgba(149,157,165,.2)', + padding: '8px' +} +function ErsatzOverlay(props: React.ComponentPropsWithoutRef<'div'>): JSX.Element { + return +} + +export function SimpleList(): JSX.Element { return ( <> -

ActionListSectionHeader

-

Filled Variant

- - - - - - - - -
repo:github/memex,github/github
-
labels:Flux,Q2
-
-
- - - - - - - - - - - - -
+

Simple List

+ + + + + + + + + + + ) +} + +export function ComplexList(): JSX.Element { + return ( + <> +

Complex List

+ + + + + + + + + + +
repo:github/memex,github/github
+
labels:Flux,Q2
+
+
+ + + + + + + + + + + + + + + +
+
) } -export function ActionListSectionHeaderStory(): JSX.Element { +export function Header(): JSX.Element { return ( <> -

ActionListSectionHeader

+

Header

Filled Variant

Subtle Variant

From 4742e8c0d02166b0a03dd030ebc35b6d5f993a43 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Tue, 9 Mar 2021 15:59:35 -0500 Subject: [PATCH 05/31] feat: Refactor to accept 'items' and 'groups' and 'renderItems' --- src/ActionList/ActionList.tsx | 15 -- src/ActionList/ActionListSection.tsx | 9 - ...tionListSectionDivider.tsx => Divider.tsx} | 13 +- src/ActionList/Group.tsx | 9 + .../{ActionListItem.tsx => Item.tsx} | 22 ++- src/ActionList/List.tsx | 42 +++++ src/ActionList/index.ts | 15 +- .../Header.tsx} | 12 +- src/ActionList/{ => private}/StyledDiv.tsx | 2 +- src/ActionList/{ => private}/StyledSpan.tsx | 2 +- src/ActionList/{ => private}/variables.ts | 0 src/stories/ActionList.stories.tsx | 156 +++++++++++------- 12 files changed, 184 insertions(+), 113 deletions(-) delete mode 100644 src/ActionList/ActionList.tsx delete mode 100644 src/ActionList/ActionListSection.tsx rename src/ActionList/{ActionListSectionDivider.tsx => Divider.tsx} (54%) create mode 100644 src/ActionList/Group.tsx rename src/ActionList/{ActionListItem.tsx => Item.tsx} (77%) create mode 100644 src/ActionList/List.tsx rename src/ActionList/{ActionListSectionHeader.tsx => private/Header.tsx} (75%) rename src/ActionList/{ => private}/StyledDiv.tsx (82%) rename src/ActionList/{ => private}/StyledSpan.tsx (82%) rename src/ActionList/{ => private}/variables.ts (100%) diff --git a/src/ActionList/ActionList.tsx b/src/ActionList/ActionList.tsx deleted file mode 100644 index e667b973dc9..00000000000 --- a/src/ActionList/ActionList.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type {SystemStyleObject} from '@styled-system/css' -import React from 'react' -import {StyledDiv} from './StyledDiv' - -export interface ActionListProps extends React.ComponentPropsWithoutRef<'div'> { - [key: string]: unknown -} - -const actionListStyles: SystemStyleObject = { - fontSize: '14px' -} - -export function ActionList(props: ActionListProps): JSX.Element { - return -} diff --git a/src/ActionList/ActionListSection.tsx b/src/ActionList/ActionListSection.tsx deleted file mode 100644 index 6effe9e5b90..00000000000 --- a/src/ActionList/ActionListSection.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' - -export interface ActionListSectionProps extends React.ComponentPropsWithoutRef<'div'> { - [key: string]: unknown -} - -export function ActionListSection(props: ActionListSectionProps): JSX.Element { - return
-} diff --git a/src/ActionList/ActionListSectionDivider.tsx b/src/ActionList/Divider.tsx similarity index 54% rename from src/ActionList/ActionListSectionDivider.tsx rename to src/ActionList/Divider.tsx index a88eefa6d70..91bb6f28757 100644 --- a/src/ActionList/ActionListSectionDivider.tsx +++ b/src/ActionList/Divider.tsx @@ -1,11 +1,9 @@ import React from 'react' import type {SystemStyleObject} from '@styled-system/css' -import {actionListItemDivider, s16, s8} from './variables' -import {StyledDiv} from './StyledDiv' +import {actionListItemDivider, s16, s8} from './private/variables' +import {StyledDiv} from './private/StyledDiv' -export interface ActionListSectionDividerProps extends React.ComponentPropsWithoutRef<'div'> { - [key: string]: unknown -} +export interface DividerProps {} const actionListSectionDividerStyles: SystemStyleObject = { position: 'relative', @@ -27,6 +25,7 @@ const actionListSectionDividerStyles: SystemStyleObject = { } } -export function ActionListSectionDivider(props: ActionListSectionDividerProps): JSX.Element { - return +export function Divider(props?: DividerProps): JSX.Element { + return } +Divider.renderItem = Divider diff --git a/src/ActionList/Group.tsx b/src/ActionList/Group.tsx new file mode 100644 index 00000000000..fc2f15dc5e3 --- /dev/null +++ b/src/ActionList/Group.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> { + [key: string]: unknown +} + +export function Group(props: GroupProps): JSX.Element { + return
+} diff --git a/src/ActionList/ActionListItem.tsx b/src/ActionList/Item.tsx similarity index 77% rename from src/ActionList/ActionListItem.tsx rename to src/ActionList/Item.tsx index 2415d0d426c..301c781c737 100644 --- a/src/ActionList/ActionListItem.tsx +++ b/src/ActionList/Item.tsx @@ -1,8 +1,8 @@ import type {IconProps} from '@primer/octicons-react' import type {SystemStyleObject} from '@styled-system/css' import React from 'react' -import {StyledDiv} from './StyledDiv' -import {StyledSpan} from './StyledSpan' +import {StyledDiv} from './private/StyledDiv' +import {StyledSpan} from './private/StyledSpan' import { borderRadius2, s6, @@ -14,9 +14,9 @@ import { textSecondary, textDanger, textSmall -} from './variables' +} from './private/variables' -export interface ActionListItemProps extends React.ComponentPropsWithoutRef<'div'> { +interface ItemPropsBase { text: string description?: string descriptionVariant?: 'inline' | 'block' @@ -25,8 +25,12 @@ export interface ActionListItemProps extends React.ComponentPropsWithoutRef<'div size?: 'small' | 'medium' | 'large' variant?: 'default' | 'singleSelection' | 'multiSelection' | 'danger' | 'static' } +interface ItemPropsWithRenderItem extends Partial { + renderItem: (props: ItemProps) => JSX.Element +} +export type ItemProps = ItemPropsBase | ItemPropsWithRenderItem -function baseStyles({variant}: {variant: ActionListItemProps['variant']}): SystemStyleObject { +function baseStyles({variant}: {variant: ItemProps['variant']}): SystemStyleObject { return { position: 'relative', display: 'flex', @@ -53,7 +57,7 @@ function baseStyles({variant}: {variant: ActionListItemProps['variant']}): Syste function textContainerStyles({ descriptionVariant }: { - descriptionVariant: ActionListItemProps['descriptionVariant'] + descriptionVariant: ItemProps['descriptionVariant'] }): SystemStyleObject { return {flexDirection: descriptionVariant === 'inline' ? 'row' : 'column'} } @@ -76,7 +80,7 @@ const descriptionStyles: SystemStyleObject = { color: textSecondary } -export function ActionListItem({ +export function Item({ text, description, descriptionVariant = 'inline', @@ -84,9 +88,9 @@ export function ActionListItem({ size: _size = 'small', variant = 'default', ...props -}: ActionListItemProps): JSX.Element { +}: ItemProps): JSX.Element { return ( - + {LeadingVisual && ( diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx new file mode 100644 index 00000000000..4a7b1b5677f --- /dev/null +++ b/src/ActionList/List.tsx @@ -0,0 +1,42 @@ +import type {SystemStyleObject} from '@styled-system/css' +import {Group, GroupProps} from './Group' +import {Item, ItemProps} from './Item' +import React from 'react' +import {StyledDiv} from './private/StyledDiv' +import {Divider} from './Divider' +import {Header, HeaderProps} from './private/Header' + +interface ListPropsWithItems { + items: I[] + renderItem?: (props: I) => JSX.Element +} +interface ListPropsWithGroups< + I extends ItemProps = ItemProps, + G extends GroupProps = GroupProps, + H extends HeaderProps = HeaderProps +> extends Partial> { + groups: (G & {header?: H; items?: I[]})[] + renderGroup?: (props: G) => JSX.Element +} +export type ListProps = ListPropsWithItems | ListPropsWithGroups + +const actionListStyles: SystemStyleObject = { + fontSize: '14px' +} + +export function List({items, renderItem = Item, ...props}: ListProps): JSX.Element { + const groups = 'groups' in props ? props.groups : [{items}] + return ( + + {groups.map(({header, items}, index) => ( + <> + + {header &&
} + {items?.map(item => ('renderItem' in item ? item.renderItem(item) : renderItem(item)))} + + {index + 1 !== groups.length && } + + ))} + + ) +} diff --git a/src/ActionList/index.ts b/src/ActionList/index.ts index 0a5d42fed3d..df6c56f8041 100644 --- a/src/ActionList/index.ts +++ b/src/ActionList/index.ts @@ -1,5 +1,10 @@ -export * from './ActionList' -export * from './ActionListItem' -export * from './ActionListSection' -export * from './ActionListSectionDivider' -export * from './ActionListSectionHeader' +import {List} from './List' +import {Group} from './Group' +import {Item} from './Item' +import {Divider} from './Divider' + +export const ActionList = Object.assign(List, { + Group, + Item, + Divider +}) diff --git a/src/ActionList/ActionListSectionHeader.tsx b/src/ActionList/private/Header.tsx similarity index 75% rename from src/ActionList/ActionListSectionHeader.tsx rename to src/ActionList/private/Header.tsx index c725ef2f4f5..a74d710332c 100644 --- a/src/ActionList/ActionListSectionHeader.tsx +++ b/src/ActionList/private/Header.tsx @@ -3,7 +3,7 @@ import {StyledDiv} from './StyledDiv' import {s8, s20, s32, gray100, gray200, textSecondary} from './variables' import type {SystemStyleObject} from '@styled-system/css' -export interface ActionListSectionHeaderProps extends React.ComponentPropsWithoutRef<'div'> { +export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'> { variant: 'subtle' | 'filled' title: string auxiliaryText?: string @@ -28,16 +28,10 @@ const filledVariantStyles: SystemStyleObject = { } } -export function ActionListSectionHeader({ - variant, - title, - auxiliaryText, - children: _children, - ...props -}: ActionListSectionHeaderProps): JSX.Element { +export function Header({variant, title, auxiliaryText, children: _children, ...props}: HeaderProps): JSX.Element { return ( & SxProp>` ${sx} diff --git a/src/ActionList/StyledSpan.tsx b/src/ActionList/private/StyledSpan.tsx similarity index 82% rename from src/ActionList/StyledSpan.tsx rename to src/ActionList/private/StyledSpan.tsx index 71aed9bd02f..56ef9a0d956 100644 --- a/src/ActionList/StyledSpan.tsx +++ b/src/ActionList/private/StyledSpan.tsx @@ -1,6 +1,6 @@ import React from 'react' import styled from 'styled-components' -import sx, {SxProp} from '../sx' +import sx, {SxProp} from '../../sx' export const StyledSpan = styled('span') & SxProp>` ${sx} diff --git a/src/ActionList/variables.ts b/src/ActionList/private/variables.ts similarity index 100% rename from src/ActionList/variables.ts rename to src/ActionList/private/variables.ts diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index 9a6d6696873..218db1f7f3b 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -1,4 +1,6 @@ import { + ServerIcon, + PlusCircleIcon, TypographyIcon, VersionsIcon, SearchIcon, @@ -12,16 +14,15 @@ import type {SystemStyleObject} from '@styled-system/css' import React from 'react' import {ThemeProvider} from 'styled-components' import {theme} from '..' -import { - ActionList, - ActionListItem, - ActionListSection, - ActionListSectionDivider, - ActionListSectionHeader -} from '../ActionList' -import {StyledDiv} from '../ActionList/StyledDiv' +import {ActionList as _ActionList} from '../ActionList' +import {Header as _Header} from '../ActionList/private/Header' +import {StyledDiv} from '../ActionList/private/StyledDiv' import BaseStyles from '../BaseStyles' +const ActionList = Object.assign(_ActionList, { + Header: _Header +}) + const meta: Meta = { title: 'Composite components/ActionList', component: ActionList, @@ -33,7 +34,12 @@ const meta: Meta = { ) - ] + ], + parameters: { + controls: { + disabled: true + } + } } export default meta @@ -46,18 +52,47 @@ function ErsatzOverlay(props: React.ComponentPropsWithoutRef<'div'>): JSX.Elemen return } +export function Actions(): JSX.Element { + return ( + <> +

Actions

+ + + + + ) +} + export function SimpleList(): JSX.Element { return ( <>

Simple List

- - - - - - - + ) @@ -68,45 +103,52 @@ export function ComplexList(): JSX.Element { <>

Complex List

- - - - - - - - - -
repo:github/memex,github/github
-
labels:Flux,Q2
-
-
- - - - - - - - - - - - - - - -
+
) @@ -117,9 +159,9 @@ export function Header(): JSX.Element { <>

Header

Filled Variant

- +

Subtle Variant

- + ) } From f09cf513ee2a6f950dce6fba348231eadfb6a238 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Tue, 9 Mar 2021 16:57:22 -0500 Subject: [PATCH 06/31] feat: Alternative implementation for 'ActionList' --- src/ActionList/private/AlternativeList.tsx | 46 ++++++++++++++ src/stories/ActionList.stories.tsx | 72 ++++++++++++++++++++-- 2 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 src/ActionList/private/AlternativeList.tsx diff --git a/src/ActionList/private/AlternativeList.tsx b/src/ActionList/private/AlternativeList.tsx new file mode 100644 index 00000000000..88e4b92cea8 --- /dev/null +++ b/src/ActionList/private/AlternativeList.tsx @@ -0,0 +1,46 @@ +import type {GroupProps} from '../Group' +import {Item, ItemProps} from '../Item' +import {List} from '../List' +import React from 'react' +import {HeaderProps} from './Header' + +interface ListPropsWithItems { + items: I[] + renderItem?: (props: I) => JSX.Element +} + +interface AlternativeListProps< + I extends ItemProps = ItemProps, + G extends GroupProps = GroupProps, + H extends HeaderProps = HeaderProps +> extends Partial> { + items: (I & {groupId: number})[] + renderItem?: (props: I) => JSX.Element + groups?: (G & {header?: H; groupId: number})[] + renderGroup?: (props: G) => JSX.Element +} + +type Flatten = T extends (infer U)[] ? U : never + +export function AlternativeList({items, renderItem = Item, ...props}: AlternativeListProps): JSX.Element { + // Coalesce 'groups' into what non-Alternative List expects + let groups: (Omit, 'groupId'> & {items?: ItemProps[]})[] | undefined = undefined + + if ('groups' in props && props.groups) { + // Create a Map of group ids to group data (including group headers) + const groupMap = props.groups.reduce( + (groups, group) => groups.set(group.groupId, group), + new Map, 'groupId'> & {items?: ItemProps[]}>() + ) + + // Add each item to its group in the Map + items.forEach(item => { + const group = groupMap.get(item.groupId) + groupMap.set(item.groupId, {...group, items: [...(group?.items ? group?.items : []), item]}) + }) + + groups = [...groupMap.values()] + } + + return +} diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index 218db1f7f3b..a136874bfc8 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -15,12 +15,14 @@ import React from 'react' import {ThemeProvider} from 'styled-components' import {theme} from '..' import {ActionList as _ActionList} from '../ActionList' -import {Header as _Header} from '../ActionList/private/Header' +import {AlternativeList as Alternative} from '../ActionList/private/AlternativeList' +import {Header} from '../ActionList/private/Header' import {StyledDiv} from '../ActionList/private/StyledDiv' import BaseStyles from '../BaseStyles' const ActionList = Object.assign(_ActionList, { - Header: _Header + Header, + Alternative }) const meta: Meta = { @@ -52,7 +54,7 @@ function ErsatzOverlay(props: React.ComponentPropsWithoutRef<'div'>): JSX.Elemen return } -export function Actions(): JSX.Element { +export function ActionsStory(): JSX.Element { return ( <>

Actions

@@ -78,8 +80,9 @@ export function Actions(): JSX.Element { ) } +ActionsStory.storyName = 'Actions' -export function SimpleList(): JSX.Element { +export function SimpleListStory(): JSX.Element { return ( <>

Simple List

@@ -97,8 +100,9 @@ export function SimpleList(): JSX.Element { ) } +SimpleListStory.storyName = 'Simple List' -export function ComplexList(): JSX.Element { +export function ComplexListStory(): JSX.Element { return ( <>

Complex List

@@ -153,8 +157,63 @@ export function ComplexList(): JSX.Element { ) } +ComplexListStory.storyName = 'Complex List' -export function Header(): JSX.Element { +export function AlternativeListStory(): JSX.Element { + return ( + <> +

Alternative List

+ + + + + ) +} +AlternativeListStory.storyName = 'Alternative List' + +export function HeaderStory(): JSX.Element { return ( <>

Header

@@ -165,3 +224,4 @@ export function Header(): JSX.Element { ) } +HeaderStory.storyName = 'Header' From 8b18e2d62d8ec97ba42a4894f682c66d22f5e231 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Fri, 12 Mar 2021 17:23:01 -0500 Subject: [PATCH 07/31] feat: Consolidate 'ActionList.Alternative' and 'ActionList' --- src/ActionList/Group.tsx | 3 +- src/ActionList/Item.tsx | 2 +- src/ActionList/List.tsx | 57 ++++++++---- src/ActionList/private/AlternativeList.tsx | 46 ---------- src/stories/ActionList.stories.tsx | 100 ++++----------------- 5 files changed, 64 insertions(+), 144 deletions(-) delete mode 100644 src/ActionList/private/AlternativeList.tsx diff --git a/src/ActionList/Group.tsx b/src/ActionList/Group.tsx index fc2f15dc5e3..9efc2d684d2 100644 --- a/src/ActionList/Group.tsx +++ b/src/ActionList/Group.tsx @@ -1,7 +1,8 @@ import React from 'react' +import {ItemProps} from './Item' export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> { - [key: string]: unknown + renderItem?: (props: ItemProps) => JSX.Element } export function Group(props: GroupProps): JSX.Element { diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 301c781c737..8abf53b6d67 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -16,7 +16,7 @@ import { textSmall } from './private/variables' -interface ItemPropsBase { +interface ItemPropsBase extends React.ComponentPropsWithoutRef<'div'> { text: string description?: string descriptionVariant?: 'inline' | 'block' diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 4a7b1b5677f..eabba749d83 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -6,33 +6,60 @@ import {StyledDiv} from './private/StyledDiv' import {Divider} from './Divider' import {Header, HeaderProps} from './private/Header' -interface ListPropsWithItems { - items: I[] - renderItem?: (props: I) => JSX.Element +type Flatten = T extends (infer U)[] ? U : never + +interface UngroupedListProps { + items: ItemProps[] + renderItem?: (props: ItemProps) => JSX.Element +} +function isUngroupedListProps(props: ListProps): props is UngroupedListProps { + return typeof props === 'object' && props !== null && !('groupMetadata' in props) +} + +interface GroupedListProps extends UngroupedListProps { + groupMetadata: (GroupProps & {groupId: number; header?: HeaderProps})[] + items: (ItemProps & {groupId: number})[] } -interface ListPropsWithGroups< - I extends ItemProps = ItemProps, - G extends GroupProps = GroupProps, - H extends HeaderProps = HeaderProps -> extends Partial> { - groups: (G & {header?: H; items?: I[]})[] - renderGroup?: (props: G) => JSX.Element +function isGroupedListProps(props: ListProps): props is GroupedListProps { + return typeof props === 'object' && props !== null && 'groupMetadata' in props } -export type ListProps = ListPropsWithItems | ListPropsWithGroups + +export type ListProps = UngroupedListProps | GroupedListProps const actionListStyles: SystemStyleObject = { fontSize: '14px' } -export function List({items, renderItem = Item, ...props}: ListProps): JSX.Element { - const groups = 'groups' in props ? props.groups : [{items}] +export function List({renderItem = Item, ...props}: ListProps): JSX.Element { + const toJSX = (itemProps: ItemProps) => + 'renderItem' in itemProps ? itemProps.renderItem(itemProps) : renderItem(itemProps) + + const groups = (() => { + if (isUngroupedListProps(props)) { + return [{items: props.items.map(toJSX)}] + } else if (isGroupedListProps(props)) { + const groupMap = props.groupMetadata.reduce( + (groups, groupMetadata) => groups.set(groupMetadata.groupId, groupMetadata), + new Map, 'groupId'> & {items?: JSX.Element[]}>() + ) + props.items.forEach(itemProps => { + const group = groupMap.get(itemProps.groupId) + groupMap.set(itemProps.groupId, { + ...group, + items: [...(group?.items ?? []), toJSX({renderItem: group?.renderItem ?? renderItem, ...itemProps})] + }) + }) + return [...groupMap.values()] + } + })() + return ( - {groups.map(({header, items}, index) => ( + {groups?.map(({header, items}, index) => ( <> {header &&
} - {items?.map(item => ('renderItem' in item ? item.renderItem(item) : renderItem(item)))} + {items} {index + 1 !== groups.length && } diff --git a/src/ActionList/private/AlternativeList.tsx b/src/ActionList/private/AlternativeList.tsx deleted file mode 100644 index 88e4b92cea8..00000000000 --- a/src/ActionList/private/AlternativeList.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type {GroupProps} from '../Group' -import {Item, ItemProps} from '../Item' -import {List} from '../List' -import React from 'react' -import {HeaderProps} from './Header' - -interface ListPropsWithItems { - items: I[] - renderItem?: (props: I) => JSX.Element -} - -interface AlternativeListProps< - I extends ItemProps = ItemProps, - G extends GroupProps = GroupProps, - H extends HeaderProps = HeaderProps -> extends Partial> { - items: (I & {groupId: number})[] - renderItem?: (props: I) => JSX.Element - groups?: (G & {header?: H; groupId: number})[] - renderGroup?: (props: G) => JSX.Element -} - -type Flatten = T extends (infer U)[] ? U : never - -export function AlternativeList({items, renderItem = Item, ...props}: AlternativeListProps): JSX.Element { - // Coalesce 'groups' into what non-Alternative List expects - let groups: (Omit, 'groupId'> & {items?: ItemProps[]})[] | undefined = undefined - - if ('groups' in props && props.groups) { - // Create a Map of group ids to group data (including group headers) - const groupMap = props.groups.reduce( - (groups, group) => groups.set(group.groupId, group), - new Map, 'groupId'> & {items?: ItemProps[]}>() - ) - - // Add each item to its group in the Map - items.forEach(item => { - const group = groupMap.get(item.groupId) - groupMap.set(item.groupId, {...group, items: [...(group?.items ? group?.items : []), item]}) - }) - - groups = [...groupMap.values()] - } - - return -} diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index a136874bfc8..48c673b5830 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -15,14 +15,12 @@ import React from 'react' import {ThemeProvider} from 'styled-components' import {theme} from '..' import {ActionList as _ActionList} from '../ActionList' -import {AlternativeList as Alternative} from '../ActionList/private/AlternativeList' import {Header} from '../ActionList/private/Header' import {StyledDiv} from '../ActionList/private/StyledDiv' import BaseStyles from '../BaseStyles' const ActionList = Object.assign(_ActionList, { - Header, - Alternative + Header }) const meta: Meta = { @@ -108,67 +106,22 @@ export function ComplexListStory(): JSX.Element {

Complex List

}, + {groupId: 4} ]} - /> - - - ) -} -ComplexListStory.storyName = 'Complex List' - -export function AlternativeListStory(): JSX.Element { - return ( - <> -

Alternative List

- - + }, { leadingVisual: NoteIcon, text: 'Table', @@ -183,35 +136,20 @@ export function AlternativeListStory(): JSX.Element { descriptionVariant: 'block', groupId: 2 }, - {leadingVisual: FilterIcon, text: 'Save sort and filters to current view', groupId: 3}, - {leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: 3}, - {leadingVisual: GearIcon, text: 'View settings', groupId: 4} - ]} - groups={[ - {groupId: 0}, - { - groupId: 1, - header: { - title: 'Live query', - variant: 'subtle' - } - }, { - groupId: 2, - header: { - title: 'Layout', - variant: 'subtle' - } + leadingVisual: FilterIcon, + text: 'Save sort and filters to current view', + groupId: 3 }, - {groupId: 3}, - {groupId: 4} + {leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: 3}, + {leadingVisual: GearIcon, text: 'View settings', groupId: 4} ]} /> ) } -AlternativeListStory.storyName = 'Alternative List' +ComplexListStory.storyName = 'Complex List' export function HeaderStory(): JSX.Element { return ( From 90859f018025b6775cde086fdacd3db57d36e132 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Tue, 16 Mar 2021 22:48:11 -0400 Subject: [PATCH 08/31] feat: Refactor to use styled-components rather than 'sx' --- src/ActionList/Divider.tsx | 40 ++++++------ src/ActionList/Header.tsx | 38 +++++++++++ src/ActionList/Item.tsx | 93 +++++++++++++-------------- src/ActionList/List.tsx | 15 ++--- src/ActionList/private/Header.tsx | 46 ------------- src/ActionList/private/StyledDiv.tsx | 7 -- src/ActionList/private/StyledSpan.tsx | 7 -- src/stories/ActionList.stories.tsx | 19 ++---- 8 files changed, 115 insertions(+), 150 deletions(-) create mode 100644 src/ActionList/Header.tsx delete mode 100644 src/ActionList/private/Header.tsx delete mode 100644 src/ActionList/private/StyledDiv.tsx delete mode 100644 src/ActionList/private/StyledSpan.tsx diff --git a/src/ActionList/Divider.tsx b/src/ActionList/Divider.tsx index 91bb6f28757..62884fb090b 100644 --- a/src/ActionList/Divider.tsx +++ b/src/ActionList/Divider.tsx @@ -1,31 +1,31 @@ import React from 'react' -import type {SystemStyleObject} from '@styled-system/css' import {actionListItemDivider, s16, s8} from './private/variables' -import {StyledDiv} from './private/StyledDiv' +import styled from 'styled-components' +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DividerProps {} -const actionListSectionDividerStyles: SystemStyleObject = { - position: 'relative', - height: '1px', - background: actionListItemDivider, - border: 0, - margin: `${s8 - 1}px ${-s8}px ${s8}px`, - padding: 0, +const StyledDivider = styled.div` + position: relative; + height: 1px; + background: ${actionListItemDivider}; + border: 0; + margin: ${s8 - 1}px -${s8}px ${s8}px; + padding: 0; - '::before': { - content: '""', - display: 'block', - position: 'absolute', - width: `calc(100% - ${s16}px)`, - height: '1px', - background: actionListItemDivider, - top: 0, - left: `${s8}px` + ::before { + content: ''; + display: block; + position: absolute; + width: calc(100% - ${s16}px); + height: 1px; + background: ${actionListItemDivider}; + top: 0; + left: ${s8}px; } -} +` export function Divider(props?: DividerProps): JSX.Element { - return + return } Divider.renderItem = Divider diff --git a/src/ActionList/Header.tsx b/src/ActionList/Header.tsx new file mode 100644 index 00000000000..7c0a7e0fb95 --- /dev/null +++ b/src/ActionList/Header.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import {s8, s20, s32, gray100, gray200, textSecondary} from './private/variables' +import styled from 'styled-components' + +export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'> { + variant: 'subtle' | 'filled' + title: string + auxiliaryText?: string +} + +const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` + padding: ${(s32 - s20) / 2}px ${s8}px; + font-size: 12px; + font-weight: bold; + color: ${textSecondary}; + + ${({variant}) => + variant === 'filled' && + ` + background: ${gray100}; + margin: ${s8}px -${s8}px; + border-top: 1px solid ${gray200}; + border-bottom: 1px solid ${gray200}; + + &:first-child { + margin-top: 0; + } + `} +` + +export function Header({variant, title, auxiliaryText, children: _children, ...props}: HeaderProps): JSX.Element { + return ( + + {title} + {auxiliaryText && auxiliaryText} + + ) +} diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 8abf53b6d67..0208331419f 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -1,8 +1,6 @@ import type {IconProps} from '@primer/octicons-react' -import type {SystemStyleObject} from '@styled-system/css' import React from 'react' -import {StyledDiv} from './private/StyledDiv' -import {StyledSpan} from './private/StyledSpan' +import styled from 'styled-components' import { borderRadius2, s6, @@ -30,55 +28,50 @@ interface ItemPropsWithRenderItem extends Partial { } export type ItemProps = ItemPropsBase | ItemPropsWithRenderItem -function baseStyles({variant}: {variant: ItemProps['variant']}): SystemStyleObject { - return { - position: 'relative', - display: 'flex', - alignItems: 'start', - borderRadius: borderRadius2, - fontWeight: 'normal', - color: variant === 'danger' ? textDanger : 'inherit', - textDecoration: 'none', - border: 'none', - background: 'none', - textAlign: 'start', - margin: 0, - padding: `${s6}px ${s8}px`, +const StyledItem = styled.div<{variant: ItemProps['variant']}>` + position: relative; + display: flex; + align-items: start; + border-radius: ${borderRadius2}; + font-weight: normal; + color: ${({variant}) => (variant === 'danger' ? textDanger : 'inherit')}; + text-decoration: none; + border: 0; + background: none; + text-align: start; + margin: 0; + padding: ${s6}px ${s8}px; - '@media (hover: hover) and (pointer: fine)': { - ':hover': { - background: variant === 'danger' ? actionListItemDangerHoverBg : actionListItemHoverBg, - cursor: 'pointer' + @media (hover: hover) and (pointer: fine) { + :hover { + background: ${props => (props.variant === 'danger' ? actionListItemDangerHoverBg : actionListItemHoverBg)}; + cursor: pointer; } } } -} +` -function textContainerStyles({ - descriptionVariant -}: { - descriptionVariant: ItemProps['descriptionVariant'] -}): SystemStyleObject { - return {flexDirection: descriptionVariant === 'inline' ? 'row' : 'column'} -} +const StyledTextContainer = styled.div<{descriptionVariant: ItemProps['descriptionVariant']}>` + flex-direction: ${({descriptionVariant}) => (descriptionVariant === 'inline' ? 'row' : 'column')}; +` -const leadingVisualStyles: SystemStyleObject = { - width: `${s16}px`, - height: `${s20}px`, - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - marginRight: `${s8}px`, +const LeadingVisualContainer = styled.span` + width: ${s16}px; + height: ${s20}px; + display: flex; + flex-direction: column; + justify-content: center; + margin-right: ${s8}px; - svg: { - fill: textSecondary, - fontSize: textSmall + svg { + fill: ${textSecondary}; + font-size: ${textSmall}; } -} +` -const descriptionStyles: SystemStyleObject = { - color: textSecondary -} +const DescriptionContainer = styled.div` + color: ${textSecondary}; +` export function Item({ text, @@ -90,16 +83,16 @@ export function Item({ ...props }: ItemProps): JSX.Element { return ( - + {LeadingVisual && ( - + - + )} - +
{text}
- {description && {description}} -
-
+ {description && {description}} + + ) } diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index eabba749d83..2c27d769f75 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -1,10 +1,9 @@ -import type {SystemStyleObject} from '@styled-system/css' import {Group, GroupProps} from './Group' import {Item, ItemProps} from './Item' import React from 'react' -import {StyledDiv} from './private/StyledDiv' import {Divider} from './Divider' -import {Header, HeaderProps} from './private/Header' +import {Header, HeaderProps} from './Header' +import styled from 'styled-components' type Flatten = T extends (infer U)[] ? U : never @@ -26,9 +25,9 @@ function isGroupedListProps(props: ListProps): props is GroupedListProps { export type ListProps = UngroupedListProps | GroupedListProps -const actionListStyles: SystemStyleObject = { - fontSize: '14px' -} +const StyledList = styled.div` + font-size: 14px; +` export function List({renderItem = Item, ...props}: ListProps): JSX.Element { const toJSX = (itemProps: ItemProps) => @@ -54,7 +53,7 @@ export function List({renderItem = Item, ...props}: ListProps): JSX.Element { })() return ( - + {groups?.map(({header, items}, index) => ( <> @@ -64,6 +63,6 @@ export function List({renderItem = Item, ...props}: ListProps): JSX.Element { {index + 1 !== groups.length && } ))} - + ) } diff --git a/src/ActionList/private/Header.tsx b/src/ActionList/private/Header.tsx deleted file mode 100644 index a74d710332c..00000000000 --- a/src/ActionList/private/Header.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react' -import {StyledDiv} from './StyledDiv' -import {s8, s20, s32, gray100, gray200, textSecondary} from './variables' -import type {SystemStyleObject} from '@styled-system/css' - -export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'> { - variant: 'subtle' | 'filled' - title: string - auxiliaryText?: string -} - -const baseStyles: SystemStyleObject = { - padding: `${(s32 - s20) / 2}px ${s8}px`, - fontSize: '12px', - fontWeight: 'bold', - color: textSecondary -} - -/** Styles used by the 'filled' variant. */ -const filledVariantStyles: SystemStyleObject = { - background: gray100, - margin: `${s8}px -${s8}px`, - borderTop: `1px solid ${gray200}`, - borderBottom: `1px solid ${gray200}`, - - '&:first-child': { - marginTop: 0 - } -} - -export function Header({variant, title, auxiliaryText, children: _children, ...props}: HeaderProps): JSX.Element { - return ( - - {title} - {auxiliaryText && auxiliaryText} - - ) -} diff --git a/src/ActionList/private/StyledDiv.tsx b/src/ActionList/private/StyledDiv.tsx deleted file mode 100644 index c266a86062d..00000000000 --- a/src/ActionList/private/StyledDiv.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import sx, {SxProp} from '../../sx' - -export const StyledDiv = styled('div') & SxProp>` - ${sx} -` diff --git a/src/ActionList/private/StyledSpan.tsx b/src/ActionList/private/StyledSpan.tsx deleted file mode 100644 index 56ef9a0d956..00000000000 --- a/src/ActionList/private/StyledSpan.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import sx, {SxProp} from '../../sx' - -export const StyledSpan = styled('span') & SxProp>` - ${sx} -` diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index 48c673b5830..8900f635139 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -10,13 +10,11 @@ import { GearIcon } from '@primer/octicons-react' import {Meta} from '@storybook/react' -import type {SystemStyleObject} from '@styled-system/css' import React from 'react' -import {ThemeProvider} from 'styled-components' +import styled, {ThemeProvider} from 'styled-components' import {theme} from '..' import {ActionList as _ActionList} from '../ActionList' -import {Header} from '../ActionList/private/Header' -import {StyledDiv} from '../ActionList/private/StyledDiv' +import {Header} from '../ActionList/Header' import BaseStyles from '../BaseStyles' const ActionList = Object.assign(_ActionList, { @@ -43,14 +41,11 @@ const meta: Meta = { } export default meta -const ersatzOverlayStyles: SystemStyleObject = { - borderRadius: '12px', - boxShadow: '0 1px 3px rgba(0,0,0,.12),0 8px 24px rgba(149,157,165,.2)', - padding: '8px' -} -function ErsatzOverlay(props: React.ComponentPropsWithoutRef<'div'>): JSX.Element { - return -} +const ErsatzOverlay = styled.div` + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 8px 24px rgba(149, 157, 165, 0.2); + padding: 8px; +` export function ActionsStory(): JSX.Element { return ( From 0754d9760ec1aa9262715dbb8a1fe26a609d09eb Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Fri, 19 Mar 2021 11:16:18 -0400 Subject: [PATCH 09/31] fix: Replace hardcoded styles with theme values --- src/ActionList/Divider.tsx | 12 +++++----- src/ActionList/Header.tsx | 32 ++++++++++++------------- src/ActionList/Item.tsx | 34 ++++++++++----------------- src/ActionList/List.tsx | 3 ++- src/ActionList/private/variables.ts | 36 ----------------------------- 5 files changed, 36 insertions(+), 81 deletions(-) delete mode 100644 src/ActionList/private/variables.ts diff --git a/src/ActionList/Divider.tsx b/src/ActionList/Divider.tsx index 62884fb090b..b9492521fba 100644 --- a/src/ActionList/Divider.tsx +++ b/src/ActionList/Divider.tsx @@ -1,6 +1,6 @@ import React from 'react' -import {actionListItemDivider, s16, s8} from './private/variables' import styled from 'styled-components' +import {get} from '../constants' // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DividerProps {} @@ -8,20 +8,20 @@ export interface DividerProps {} const StyledDivider = styled.div` position: relative; height: 1px; - background: ${actionListItemDivider}; + background: ${get('colors.selectMenu.borderSecondary')}; border: 0; - margin: ${s8 - 1}px -${s8}px ${s8}px; + margin: calc(${get('space.2')} - 1px) -${get('space.2')} ${get('space.2')}; padding: 0; ::before { content: ''; display: block; position: absolute; - width: calc(100% - ${s16}px); + width: calc(100% - ${get('space.3')}); height: 1px; - background: ${actionListItemDivider}; + background: ${get('colors.selectMenu.borderSecondary')}; top: 0; - left: ${s8}px; + left: ${get('space.2')}; } ` diff --git a/src/ActionList/Header.tsx b/src/ActionList/Header.tsx index 7c0a7e0fb95..01a85890614 100644 --- a/src/ActionList/Header.tsx +++ b/src/ActionList/Header.tsx @@ -1,6 +1,6 @@ import React from 'react' -import {s8, s20, s32, gray100, gray200, textSecondary} from './private/variables' -import styled from 'styled-components' +import styled, {css} from 'styled-components' +import {get} from '../constants' export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'> { variant: 'subtle' | 'filled' @@ -9,23 +9,23 @@ export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'> { } const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` - padding: ${(s32 - s20) / 2}px ${s8}px; - font-size: 12px; - font-weight: bold; - color: ${textSecondary}; + padding: calc((${get('space.3')} - ${get('space.1')}) / 2) ${get('space.2')}; + font-size: ${get('fontSizes.0')}; + font-weight: ${get('fontWeights.bold')}; + color: ${get('colors.text.secondary')}; ${({variant}) => variant === 'filled' && - ` - background: ${gray100}; - margin: ${s8}px -${s8}px; - border-top: 1px solid ${gray200}; - border-bottom: 1px solid ${gray200}; - - &:first-child { - margin-top: 0; - } - `} + css` + background: ${get('colors.bg.gray')}; + margin: ${get('space.2')} -${get('space.2')}; + border-top: 1px solid ${get('colors.border.gray')}; + border-bottom: 1px solid ${get('colors.border.gray')}; + + &:first-child { + margin-top: 0; + } + `} ` export function Header({variant, title, auxiliaryText, children: _children, ...props}: HeaderProps): JSX.Element { diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 0208331419f..e67b4a1095a 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -1,18 +1,7 @@ import type {IconProps} from '@primer/octicons-react' import React from 'react' import styled from 'styled-components' -import { - borderRadius2, - s6, - s8, - s16, - s20, - actionListItemHoverBg, - actionListItemDangerHoverBg, - textSecondary, - textDanger, - textSmall -} from './private/variables' +import {get} from '../constants' interface ItemPropsBase extends React.ComponentPropsWithoutRef<'div'> { text: string @@ -32,19 +21,20 @@ const StyledItem = styled.div<{variant: ItemProps['variant']}>` position: relative; display: flex; align-items: start; - border-radius: ${borderRadius2}; + border-radius: ${get('radii.2')}; font-weight: normal; - color: ${({variant}) => (variant === 'danger' ? textDanger : 'inherit')}; + color: ${({variant}) => (variant === 'danger' ? get('colors.text.danger') : 'inherit')}; text-decoration: none; border: 0; background: none; text-align: start; margin: 0; - padding: ${s6}px ${s8}px; + padding: calc((${get('space.3')} - ${get('space.1')}) / 2) ${get('space.2')}; @media (hover: hover) and (pointer: fine) { :hover { - background: ${props => (props.variant === 'danger' ? actionListItemDangerHoverBg : actionListItemHoverBg)}; + background: ${props => + props.variant === 'danger' ? get('colors.bg.danger') : get('colors.selectMenu.tapHighlight')}; cursor: pointer; } } @@ -56,21 +46,21 @@ const StyledTextContainer = styled.div<{descriptionVariant: ItemProps['descripti ` const LeadingVisualContainer = styled.span` - width: ${s16}px; - height: ${s20}px; + width: ${get('space.3')}; + height: calc(${get('space.4')} - ${get('space.1')}); display: flex; flex-direction: column; justify-content: center; - margin-right: ${s8}px; + margin-right: ${get('space.2')}; svg { - fill: ${textSecondary}; - font-size: ${textSmall}; + fill: ${get('colors.text.secondary')}; + font-size: ${get('fontSizes.0')}; } ` const DescriptionContainer = styled.div` - color: ${textSecondary}; + color: ${get('colors.text.secondary')}; ` export function Item({ diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 2c27d769f75..5283bfe17f0 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -4,6 +4,7 @@ import React from 'react' import {Divider} from './Divider' import {Header, HeaderProps} from './Header' import styled from 'styled-components' +import {get} from '../constants' type Flatten = T extends (infer U)[] ? U : never @@ -26,7 +27,7 @@ function isGroupedListProps(props: ListProps): props is GroupedListProps { export type ListProps = UngroupedListProps | GroupedListProps const StyledList = styled.div` - font-size: 14px; + font-size: ${get('fontSizes.1')}; ` export function List({renderItem = Item, ...props}: ListProps): JSX.Element { diff --git a/src/ActionList/private/variables.ts b/src/ActionList/private/variables.ts deleted file mode 100644 index 496b1148d97..00000000000 --- a/src/ActionList/private/variables.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Most of this file should be provided by primer/src/support already - -export const actionListItemHoverBg = '#F0F3F5' -export const actionListItemDivider = '#E9E9ED' - -export const actionListItemDangerHoverBg = '#FFF0F2' - -// expanded from src/support/variables/misc.scss - -export const borderRadius2 = '6px' - -// expanded from src/support/variables/layout.scss -// adds 12px to the scale as of https://github.com/github/design-systems/issues/992 -// -// https://github.com/github/design-systems/issues/1272 - -export const s6 = 6 -export const s8 = 8 -export const s16 = 16 -export const s20 = 20 -export const s32 = 32 - -// pulled from src/support/variables/color-system.scss - -// -------- Grays -------- - -export const gray100 = '#f6f8fa' -export const gray200 = '#e1e4e8' -export const gray500 = '#6a737d' - -// Typographic colors, from some color mode file - -export const textSecondary = gray500 -export const textDanger = '#CB2431' - -export const textSmall = '12px' From d78eb83dbed23512bebdd43395c8909ca890df11 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Fri, 19 Mar 2021 11:43:08 -0400 Subject: [PATCH 10/31] tests: Add tests for 'ActionList' --- src/ActionList/List.tsx | 2 +- src/__tests__/ActionList.tsx | 45 +++++++++++++++++++ .../__snapshots__/ActionList.tsx.snap | 16 +++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/ActionList.tsx create mode 100644 src/__tests__/__snapshots__/ActionList.tsx.snap diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 5283bfe17f0..3f5a2b57bd2 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -36,7 +36,7 @@ export function List({renderItem = Item, ...props}: ListProps): JSX.Element { const groups = (() => { if (isUngroupedListProps(props)) { - return [{items: props.items.map(toJSX)}] + return [{items: props.items?.map(toJSX)}] } else if (isGroupedListProps(props)) { const groupMap = props.groupMetadata.reduce( (groups, groupMetadata) => groups.set(groupMetadata.groupId, groupMetadata), diff --git a/src/__tests__/ActionList.tsx b/src/__tests__/ActionList.tsx new file mode 100644 index 00000000000..5add974d2db --- /dev/null +++ b/src/__tests__/ActionList.tsx @@ -0,0 +1,45 @@ +import {cleanup, render as HTMLRender} from '@testing-library/react' +import 'babel-polyfill' +import {axe, toHaveNoViolations} from 'jest-axe' +import React from 'react' +import theme from '../theme' +import {ActionList} from '../ActionList' +import {COMMON} from '../constants' +import {behavesAsComponent, checkExports} from '../utils/testing' +import {ThemeProvider} from 'styled-components' +import {BaseStyles} from '..' +expect.extend(toHaveNoViolations) + +function SimpleActionList(): JSX.Element { + return ( + + + + + + ) +} + +describe('ActionList', () => { + behavesAsComponent({Component: ActionList, systemPropArray: [COMMON], options: {skipAs: true, skipSx: true}}) + + checkExports('ActionList', { + default: undefined, + ActionList + }) + + it('should have no axe violations', async () => { + const {container} = HTMLRender() + const results = await axe(container) + expect(results).toHaveNoViolations() + cleanup() + }) +}) diff --git a/src/__tests__/__snapshots__/ActionList.tsx.snap b/src/__tests__/__snapshots__/ActionList.tsx.snap new file mode 100644 index 00000000000..507f6e03fce --- /dev/null +++ b/src/__tests__/__snapshots__/ActionList.tsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ActionList renders consistently 1`] = ` +.c0 { + font-size: 14px; +} + +
+
+
+`; From b95c3ae7482125e98b80a95eae8d579ec9e2675f Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Fri, 19 Mar 2021 11:53:44 -0400 Subject: [PATCH 11/31] docs: Document ActionList --- docs/content/ActionList.mdx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/content/ActionList.mdx diff --git a/docs/content/ActionList.mdx b/docs/content/ActionList.mdx new file mode 100644 index 00000000000..a24cb499438 --- /dev/null +++ b/docs/content/ActionList.mdx @@ -0,0 +1,33 @@ +--- +title: ActionList +--- + +An `ActionList` is a list of items which can be activated or selected. `ActionList` is the base component for many of our menu-type components, including `DropdownMenu` and `ActionMenu`. + +## Default example + +```javascript live noinline +const Demo = () => { + return ( + + ) +} + +render() +``` + +## Component props + +| Name | Type | Default | Description | +| :------------ | :---------------------------------- | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------ | +| items | `ItemProps[]` | `undefined` | Required. A list of item objects conforming to the `ActionList.Item` props interface. | +| renderItems | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for `ActionList`-wide custom item rendering. | +| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `ActionList` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | From d92d5552068368742de614aef522e564e88c4226 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Mon, 29 Mar 2021 13:34:05 -0400 Subject: [PATCH 12/31] fix: Remove unneeded type guard. Remove IIFE. --- src/ActionList/List.tsx | 44 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 3f5a2b57bd2..840eaa3c5d0 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -12,46 +12,44 @@ interface UngroupedListProps { items: ItemProps[] renderItem?: (props: ItemProps) => JSX.Element } -function isUngroupedListProps(props: ListProps): props is UngroupedListProps { - return typeof props === 'object' && props !== null && !('groupMetadata' in props) -} interface GroupedListProps extends UngroupedListProps { groupMetadata: (GroupProps & {groupId: number; header?: HeaderProps})[] items: (ItemProps & {groupId: number})[] } function isGroupedListProps(props: ListProps): props is GroupedListProps { - return typeof props === 'object' && props !== null && 'groupMetadata' in props + return 'groupMetadata' in props } export type ListProps = UngroupedListProps | GroupedListProps +type GroupWithItems = Omit, 'groupId'> & {items?: JSX.Element[]} + const StyledList = styled.div` font-size: ${get('fontSizes.1')}; ` -export function List({renderItem = Item, ...props}: ListProps): JSX.Element { +export function List(props: ListProps): JSX.Element { const toJSX = (itemProps: ItemProps) => - 'renderItem' in itemProps ? itemProps.renderItem(itemProps) : renderItem(itemProps) + ((('renderItem' in itemProps && itemProps.renderItem) ?? props.renderItem) || Item).call(null, itemProps) - const groups = (() => { - if (isUngroupedListProps(props)) { - return [{items: props.items?.map(toJSX)}] - } else if (isGroupedListProps(props)) { - const groupMap = props.groupMetadata.reduce( - (groups, groupMetadata) => groups.set(groupMetadata.groupId, groupMetadata), - new Map, 'groupId'> & {items?: JSX.Element[]}>() - ) - props.items.forEach(itemProps => { - const group = groupMap.get(itemProps.groupId) - groupMap.set(itemProps.groupId, { - ...group, - items: [...(group?.items ?? []), toJSX({renderItem: group?.renderItem ?? renderItem, ...itemProps})] - }) + let groups: GroupWithItems[] = [] + if (!isGroupedListProps(props)) { + groups = [{items: props.items?.map(toJSX)}] + } else { + const groupMap = props.groupMetadata.reduce( + (groups, groupMetadata) => groups.set(groupMetadata.groupId, groupMetadata), + new Map() + ) + props.items.forEach(itemProps => { + const group = groupMap.get(itemProps.groupId) + groupMap.set(itemProps.groupId, { + ...group, + items: [...(group?.items ?? []), toJSX({renderItem: group?.renderItem ?? props.renderItem, ...itemProps})] }) - return [...groupMap.values()] - } - })() + }) + groups = [...groupMap.values()] + } return ( From 7f321c6a80859ccd016e86b2e15d32b21b849033 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Mon, 29 Mar 2021 13:37:30 -0400 Subject: [PATCH 13/31] =?UTF-8?q?fix:=20Replace=20'forEach'=20with=20'for?= =?UTF-8?q?=E2=80=A6of'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ActionList/List.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 840eaa3c5d0..8ef8e8bc1f1 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -41,13 +41,13 @@ export function List(props: ListProps): JSX.Element { (groups, groupMetadata) => groups.set(groupMetadata.groupId, groupMetadata), new Map() ) - props.items.forEach(itemProps => { + for (const itemProps of props.items) { const group = groupMap.get(itemProps.groupId) groupMap.set(itemProps.groupId, { ...group, items: [...(group?.items ?? []), toJSX({renderItem: group?.renderItem ?? props.renderItem, ...itemProps})] }) - }) + } groups = [...groupMap.values()] } From f95e1a0aa9189ced510cd408bf1e6bff83c6a90c Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Mon, 29 Mar 2021 13:49:40 -0400 Subject: [PATCH 14/31] fix: Use string groupIds instead of number groupIds per t-hugs\' suggestion --- src/ActionList/List.tsx | 6 +++--- src/stories/ActionList.stories.tsx | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 8ef8e8bc1f1..60e31b301fb 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -14,8 +14,8 @@ interface UngroupedListProps { } interface GroupedListProps extends UngroupedListProps { - groupMetadata: (GroupProps & {groupId: number; header?: HeaderProps})[] - items: (ItemProps & {groupId: number})[] + groupMetadata: (GroupProps & {groupId: string; header?: HeaderProps})[] + items: (ItemProps & {groupId: string})[] } function isGroupedListProps(props: ListProps): props is GroupedListProps { return 'groupMetadata' in props @@ -39,7 +39,7 @@ export function List(props: ListProps): JSX.Element { } else { const groupMap = props.groupMetadata.reduce( (groups, groupMetadata) => groups.set(groupMetadata.groupId, groupMetadata), - new Map() + new Map() ) for (const itemProps of props.items) { const group = groupMap.get(itemProps.groupId) diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index 8900f635139..9c13ea2807c 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -102,19 +102,19 @@ export function ComplexListStory(): JSX.Element { }, - {groupId: 4} + {groupId: '0'}, + {groupId: '1', header: {title: 'Live query', variant: 'subtle'}}, + {groupId: '2', header: {title: 'Layout', variant: 'subtle'}}, + {groupId: '3', renderItem: props => }, + {groupId: '4'} ]} items={[ - {leadingVisual: TypographyIcon, text: 'Rename', groupId: 0}, - {leadingVisual: VersionsIcon, text: 'Duplicate', groupId: 0}, + {leadingVisual: TypographyIcon, text: 'Rename', groupId: '0'}, + {leadingVisual: VersionsIcon, text: 'Duplicate', groupId: '0'}, { leadingVisual: SearchIcon, text: 'repo:github/memex,github/github', - groupId: 1, + groupId: '1', renderItem: props => }, { @@ -122,22 +122,22 @@ export function ComplexListStory(): JSX.Element { text: 'Table', description: 'Information-dense table optimized for operations across teams', descriptionVariant: 'block', - groupId: 2 + groupId: '2' }, { leadingVisual: ProjectIcon, text: 'Board', description: 'Kanban-style board focused on visual states', descriptionVariant: 'block', - groupId: 2 + groupId: '2' }, { leadingVisual: FilterIcon, text: 'Save sort and filters to current view', - groupId: 3 + groupId: '3' }, - {leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: 3}, - {leadingVisual: GearIcon, text: 'View settings', groupId: 4} + {leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: '3'}, + {leadingVisual: GearIcon, text: 'View settings', groupId: '4'} ]} /> From 78ed7353bcefda20998b5a53e802e9504d3e76bb Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Mon, 29 Mar 2021 14:28:22 -0400 Subject: [PATCH 15/31] fix: Remove 'data-component' --- src/ActionList/Divider.tsx | 2 +- src/ActionList/Group.tsx | 4 ++-- src/ActionList/Header.tsx | 2 +- src/ActionList/Item.tsx | 2 +- src/ActionList/List.tsx | 2 +- src/__tests__/__snapshots__/ActionList.tsx.snap | 5 +---- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/ActionList/Divider.tsx b/src/ActionList/Divider.tsx index b9492521fba..506e24ff4e0 100644 --- a/src/ActionList/Divider.tsx +++ b/src/ActionList/Divider.tsx @@ -26,6 +26,6 @@ const StyledDivider = styled.div` ` export function Divider(props?: DividerProps): JSX.Element { - return + return } Divider.renderItem = Divider diff --git a/src/ActionList/Group.tsx b/src/ActionList/Group.tsx index 9efc2d684d2..aab23cea1a2 100644 --- a/src/ActionList/Group.tsx +++ b/src/ActionList/Group.tsx @@ -5,6 +5,6 @@ export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> { renderItem?: (props: ItemProps) => JSX.Element } -export function Group(props: GroupProps): JSX.Element { - return
+export function Group({renderItem: _renderItem, ...props}: GroupProps): JSX.Element { + return
} diff --git a/src/ActionList/Header.tsx b/src/ActionList/Header.tsx index 01a85890614..3ea9ce766d3 100644 --- a/src/ActionList/Header.tsx +++ b/src/ActionList/Header.tsx @@ -30,7 +30,7 @@ const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` export function Header({variant, title, auxiliaryText, children: _children, ...props}: HeaderProps): JSX.Element { return ( - + {title} {auxiliaryText && auxiliaryText} diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index e67b4a1095a..e05e053ba4d 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -73,7 +73,7 @@ export function Item({ ...props }: ItemProps): JSX.Element { return ( - + {LeadingVisual && ( diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 60e31b301fb..7df04365319 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -52,7 +52,7 @@ export function List(props: ListProps): JSX.Element { } return ( - + {groups?.map(({header, items}, index) => ( <> diff --git a/src/__tests__/__snapshots__/ActionList.tsx.snap b/src/__tests__/__snapshots__/ActionList.tsx.snap index 507f6e03fce..60323046e0f 100644 --- a/src/__tests__/__snapshots__/ActionList.tsx.snap +++ b/src/__tests__/__snapshots__/ActionList.tsx.snap @@ -7,10 +7,7 @@ exports[`ActionList renders consistently 1`] = `
-
+
`; From 3397684e4d6908ccfd6328af820e8185540bf359 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Mon, 29 Mar 2021 14:39:23 -0400 Subject: [PATCH 16/31] fix: Use 'div' instead of 'span' for 'display: flex' leading visual containers --- src/ActionList/Item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index e05e053ba4d..201155e8727 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -45,7 +45,7 @@ const StyledTextContainer = styled.div<{descriptionVariant: ItemProps['descripti flex-direction: ${({descriptionVariant}) => (descriptionVariant === 'inline' ? 'row' : 'column')}; ` -const LeadingVisualContainer = styled.span` +const LeadingVisualContainer = styled.div` width: ${get('space.3')}; height: calc(${get('space.4')} - ${get('space.1')}); display: flex; From 9aa05fa50516b7c5deee7da99a328be1901b5b71 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Mon, 29 Mar 2021 15:12:04 -0400 Subject: [PATCH 17/31] fix: Use 'span' instead of 'div' for phrasing content Item description --- src/ActionList/Item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 201155e8727..98e72b21b98 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -59,7 +59,7 @@ const LeadingVisualContainer = styled.div` } ` -const DescriptionContainer = styled.div` +const DescriptionContainer = styled.span` color: ${get('colors.text.secondary')}; ` From b177ae65e72badaea687915109b61e44851b4596 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Mon, 29 Mar 2021 15:32:00 -0400 Subject: [PATCH 18/31] fix: Use 'ThemeProvider' from Primer, not from 'styled-components' --- src/stories/ActionList.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index 9c13ea2807c..ae5add1151e 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -11,8 +11,8 @@ import { } from '@primer/octicons-react' import {Meta} from '@storybook/react' import React from 'react' -import styled, {ThemeProvider} from 'styled-components' -import {theme} from '..' +import styled from 'styled-components' +import {theme, ThemeProvider} from '..' import {ActionList as _ActionList} from '../ActionList' import {Header} from '../ActionList/Header' import BaseStyles from '../BaseStyles' From 90fea7a1ef3631bf407106bdb308145c1d6998f5 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Mon, 29 Mar 2021 16:20:58 -0400 Subject: [PATCH 19/31] fix: Remove unused styles --- src/ActionList/Divider.tsx | 21 ++------------------- src/ActionList/Item.tsx | 8 -------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/ActionList/Divider.tsx b/src/ActionList/Divider.tsx index 506e24ff4e0..84e086a10fc 100644 --- a/src/ActionList/Divider.tsx +++ b/src/ActionList/Divider.tsx @@ -2,30 +2,13 @@ import React from 'react' import styled from 'styled-components' import {get} from '../constants' -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DividerProps {} - const StyledDivider = styled.div` - position: relative; height: 1px; background: ${get('colors.selectMenu.borderSecondary')}; - border: 0; margin: calc(${get('space.2')} - 1px) -${get('space.2')} ${get('space.2')}; - padding: 0; - - ::before { - content: ''; - display: block; - position: absolute; - width: calc(100% - ${get('space.3')}); - height: 1px; - background: ${get('colors.selectMenu.borderSecondary')}; - top: 0; - left: ${get('space.2')}; - } ` -export function Divider(props?: DividerProps): JSX.Element { - return +export function Divider(): JSX.Element { + return } Divider.renderItem = Divider diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 98e72b21b98..9d00253d7b6 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -18,17 +18,9 @@ interface ItemPropsWithRenderItem extends Partial { export type ItemProps = ItemPropsBase | ItemPropsWithRenderItem const StyledItem = styled.div<{variant: ItemProps['variant']}>` - position: relative; display: flex; - align-items: start; border-radius: ${get('radii.2')}; - font-weight: normal; color: ${({variant}) => (variant === 'danger' ? get('colors.text.danger') : 'inherit')}; - text-decoration: none; - border: 0; - background: none; - text-align: start; - margin: 0; padding: calc((${get('space.3')} - ${get('space.1')}) / 2) ${get('space.2')}; @media (hover: hover) and (pointer: fine) { From 69a489b3218f28561b7613a8169fe48b9061c98a Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Tue, 30 Mar 2021 11:57:22 -0400 Subject: [PATCH 20/31] docs: Remove 'render' from 'ActionList' docs example Co-authored-by: Cole Bemis --- docs/content/ActionList.mdx | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/content/ActionList.mdx b/docs/content/ActionList.mdx index a24cb499438..6764a4da749 100644 --- a/docs/content/ActionList.mdx +++ b/docs/content/ActionList.mdx @@ -6,22 +6,16 @@ An `ActionList` is a list of items which can be activated or selected. `ActionLi ## Default example -```javascript live noinline -const Demo = () => { - return ( - - ) -} - -render() +```jsx live + ``` ## Component props From 0552613dc0791f10e107abd31744753d47cf2473 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Tue, 30 Mar 2021 11:59:53 -0400 Subject: [PATCH 21/31] fix: Remove redundant 'theme' prop from 'ThemeProvider' in Storybook story Co-authored-by: Cole Bemis --- src/stories/ActionList.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index ae5add1151e..e8b9b47c9e2 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -26,7 +26,7 @@ const meta: Meta = { component: ActionList, decorators: [ (Story: React.ComponentType): JSX.Element => ( - + From 69f1cf0676ec5e277a3e75ccad27bd9a78b1246c Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Tue, 30 Mar 2021 23:51:32 -0400 Subject: [PATCH 22/31] docs: Explain every 'ActionList' export --- src/ActionList/Divider.tsx | 10 +++++ src/ActionList/Group.tsx | 11 ++++++ src/ActionList/Header.tsx | 20 ++++++++++ src/ActionList/Item.tsx | 47 ++++++++++++++++++++-- src/ActionList/List.tsx | 62 +++++++++++++++++++++++++++++- src/ActionList/index.ts | 11 ++++++ src/index.ts | 1 + src/stories/ActionList.stories.tsx | 2 +- src/utils/types.ts | 5 +++ 9 files changed, 162 insertions(+), 7 deletions(-) diff --git a/src/ActionList/Divider.tsx b/src/ActionList/Divider.tsx index 84e086a10fc..fbe37111a78 100644 --- a/src/ActionList/Divider.tsx +++ b/src/ActionList/Divider.tsx @@ -8,7 +8,17 @@ const StyledDivider = styled.div` margin: calc(${get('space.2')} - 1px) -${get('space.2')} ${get('space.2')}; ` +/** + * Visually separates `Item`s or `Group`s in an `ActionList`. + */ export function Divider(): JSX.Element { return } + +/** + * `Divider` fulfills the `ItemPropsWithRenderItem` contract, + * so it can be used inline in an `ActionList`’s `items` prop. + * In other words, `items={[ActionList.Divider]}` is supported as a concise + * alternative to `items={[{renderItem: () => }]}`. + */ Divider.renderItem = Divider diff --git a/src/ActionList/Group.tsx b/src/ActionList/Group.tsx index aab23cea1a2..a4ea3ab599f 100644 --- a/src/ActionList/Group.tsx +++ b/src/ActionList/Group.tsx @@ -1,10 +1,21 @@ import React from 'react' import {ItemProps} from './Item' +/** + * Contract for props passed to the `Group` component. + */ export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> { + /** + * A `Group`-level custom `Item` renderer. Every `Item` within this `Group` + * without an `Item`-level custom `Item` renderer will be rendered using + * this function component. + */ renderItem?: (props: ItemProps) => JSX.Element } +/** + * Collects related `Items` in an `ActionList`. + */ export function Group({renderItem: _renderItem, ...props}: GroupProps): JSX.Element { return
} diff --git a/src/ActionList/Header.tsx b/src/ActionList/Header.tsx index 3ea9ce766d3..8e73dec6bc2 100644 --- a/src/ActionList/Header.tsx +++ b/src/ActionList/Header.tsx @@ -2,9 +2,26 @@ import React from 'react' import styled, {css} from 'styled-components' import {get} from '../constants' +/** + * Contract for props passed to the `Header` component. + */ export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'> { + /** + * Style variations. Usage is discretionary. + * + * - `"filled"` - Superimposed on a background, offset from nearby content + * - `"subtle"` - Relatively less offset from nearby content + */ variant: 'subtle' | 'filled' + + /** + * Primary text which names a `Group`. + */ title: string + + /** + * Secondary text which provides additional information about a `Group`. + */ auxiliaryText?: string } @@ -28,6 +45,9 @@ const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` `} ` +/** + * Displays the name and description of a `Group`. + */ export function Header({variant, title, auxiliaryText, children: _children, ...props}: HeaderProps): JSX.Element { return ( diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 9d00253d7b6..05f3df7aac3 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -3,18 +3,55 @@ import React from 'react' import styled from 'styled-components' import {get} from '../constants' +/** + * Contract for props passed to the `Item` component. + */ interface ItemPropsBase extends React.ComponentPropsWithoutRef<'div'> { + /** + * Primary text which names an `Item`. + */ text: string + + /** + * Secondary text which provides additional information about an `Item`. + */ description?: string + + /** + * Secondary text style variations. Usage is discretionary. + * + * - `"inline"` - Secondary text is positioned beside primary text. + * - `"block"` - Secondary text is positioned below primary text. + */ descriptionVariant?: 'inline' | 'block' + + /** + * Icon (or similar) positioned before `Item` text. + */ leadingVisual?: React.FunctionComponent - leadingVisualSize?: 16 | 20 - size?: 'small' | 'medium' | 'large' - variant?: 'default' | 'singleSelection' | 'multiSelection' | 'danger' | 'static' + + /** + * Style variations associated with various `Item` types. + * + * - `"default"` - An action `Item`. + * - `"danger"` - A destructive action `Item`. + */ + variant?: 'default' | 'danger' } + +/** + * Contract for props passed to the `Item` component, when an `Item`-level custom `Item` renderer is used. + */ interface ItemPropsWithRenderItem extends Partial { + /** + * An `Item`-level custom `Item` renderer. + */ renderItem: (props: ItemProps) => JSX.Element } + +/** + * Contract for props passed to the `Item` component. + */ export type ItemProps = ItemPropsBase | ItemPropsWithRenderItem const StyledItem = styled.div<{variant: ItemProps['variant']}>` @@ -55,12 +92,14 @@ const DescriptionContainer = styled.span` color: ${get('colors.text.secondary')}; ` +/** + * An actionable or selectable `Item` with an optional icon and description. + */ export function Item({ text, description, descriptionVariant = 'inline', leadingVisual: LeadingVisual, - size: _size = 'small', variant = 'default', ...props }: ItemProps): JSX.Element { diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 7df04365319..9ac38dfcc7b 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -5,49 +5,107 @@ import {Divider} from './Divider' import {Header, HeaderProps} from './Header' import styled from 'styled-components' import {get} from '../constants' +import type {Flatten} from '../utils/types' -type Flatten = T extends (infer U)[] ? U : never - +/** + * Contract for props passed to the `List` component. + */ interface UngroupedListProps { + /** + * `Item`s to render in the `List`. + */ items: ItemProps[] + + /** + * A `List`-level custom `Item` renderer. Every `Item` within this `List` + * without a `Group`-level or `Item`-level custom `Item` renderer will be + * rendered using this function component. + */ renderItem?: (props: ItemProps) => JSX.Element } +/** + * Contract for props passed to the `List` component, when its `Item`s are collected in `Group`s. + */ interface GroupedListProps extends UngroupedListProps { + /** + * An array of `Group`s, each an associated `Header` and a group idenitifier. + */ groupMetadata: (GroupProps & {groupId: string; header?: HeaderProps})[] + + /** + * `Items` to render in the `List`, each with a group identifier used to associate it with its `Group`. + */ items: (ItemProps & {groupId: string})[] } + +/** + * Asserts that the given value fulfills the `GroupedListProps` contract. + * @param props A value which fulfills either the `UngroupedListProps` or the `GroupedListProps` contract. + */ function isGroupedListProps(props: ListProps): props is GroupedListProps { return 'groupMetadata' in props } +/** + * Contract for props passed to the `List` component. + */ export type ListProps = UngroupedListProps | GroupedListProps +/** + * An array of `Group`s, each with an associated `Header` and with an array of `Item`s belonging to that `Group`. + */ type GroupWithItems = Omit, 'groupId'> & {items?: JSX.Element[]} const StyledList = styled.div` font-size: ${get('fontSizes.1')}; ` +/** + * Lists `Item`s, either grouped or ungrouped, with a `Divider` between each group. + */ export function List(props: ListProps): JSX.Element { + /** + * Render an `Item` using the first of the following renderers that is defined: + * An `Item`-level, `Group`-level, or `List`-level custom `Item` renderer, + * or the default `Item` renderer. + */ const toJSX = (itemProps: ItemProps) => ((('renderItem' in itemProps && itemProps.renderItem) ?? props.renderItem) || Item).call(null, itemProps) + /** + * An array of `Group`s, each with an associated `Header` and with an array of `Item`s belonging to that `Group`. + */ let groups: GroupWithItems[] = [] + + // Collect rendered `Item`s into `Group`s, avoiding excess iteration over the lists of `items` and `groupMetadata`: + if (!isGroupedListProps(props)) { + // When no `groupMetadata`s is provided, collect rendered `Item`s into a single anonymous `Group`. groups = [{items: props.items?.map(toJSX)}] + } else { + // When `groupMetadata` is provided, collect rendered `Item`s into their associated `Group`s. + + /** + * A map of group identifiers to `Group`s, each with an associated `Header` and an array of `Item`s belonging to that `Group`. + */ const groupMap = props.groupMetadata.reduce( (groups, groupMetadata) => groups.set(groupMetadata.groupId, groupMetadata), new Map() ) + for (const itemProps of props.items) { + // Look up the group associated with the current item. const group = groupMap.get(itemProps.groupId) + + // Upsert the group to include the current item (rendered). groupMap.set(itemProps.groupId, { ...group, items: [...(group?.items ?? []), toJSX({renderItem: group?.renderItem ?? props.renderItem, ...itemProps})] }) } + groups = [...groupMap.values()] } diff --git a/src/ActionList/index.ts b/src/ActionList/index.ts index df6c56f8041..3ce2bfcb59a 100644 --- a/src/ActionList/index.ts +++ b/src/ActionList/index.ts @@ -1,10 +1,21 @@ import {List} from './List' +export type {ListProps as ActionListProps} from './List' import {Group} from './Group' +export type {GroupProps} from './Group' import {Item} from './Item' +export type {ItemProps} from './Item' import {Divider} from './Divider' +/** + * Collection of list-related components. + */ export const ActionList = Object.assign(List, { + /** Collects related `Items` in an `ActionList`. */ Group, + + /** An actionable or selectable `Item` with an optional icon and description. */ Item, + + /** Visually separates `Item`s or `Group`s in an `ActionList`. */ Divider }) diff --git a/src/index.ts b/src/index.ts index 21322ab6686..75d11da09fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ export {default as useMouseIntent} from './hooks/useMouseIntent' export {default as useSafeTimeout} from './hooks/useSafeTimeout' // Components +export {ActionList} from './ActionList' export {default as Avatar} from './Avatar' export {default as AvatarPair} from './AvatarPair' export {default as AvatarStack} from './AvatarStack' diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index e8b9b47c9e2..e380d16056c 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -12,7 +12,7 @@ import { import {Meta} from '@storybook/react' import React from 'react' import styled from 'styled-components' -import {theme, ThemeProvider} from '..' +import {ThemeProvider} from '..' import {ActionList as _ActionList} from '../ActionList' import {Header} from '../ActionList/Header' import BaseStyles from '../BaseStyles' diff --git a/src/utils/types.ts b/src/utils/types.ts index 72c4c38199c..ddee59a7c73 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -10,3 +10,8 @@ export type ComponentProps = T extends React.ComponentType ? Props : never : never + +/** + * Contruct a type describing the items in `T`, if `T` is an array. + */ +export type Flatten = T extends (infer U)[] ? U : never \ No newline at end of file From 04c0648edc6a83fc56e5028bdf9ebb6e3fa4a797 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Wed, 31 Mar 2021 12:18:18 -0400 Subject: [PATCH 23/31] fix: Add 'ActionList' example featuring grouped items. --- docs/content/ActionList.mdx | 42 ++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/content/ActionList.mdx b/docs/content/ActionList.mdx index 6764a4da749..51e77f16699 100644 --- a/docs/content/ActionList.mdx +++ b/docs/content/ActionList.mdx @@ -4,7 +4,7 @@ title: ActionList An `ActionList` is a list of items which can be activated or selected. `ActionList` is the base component for many of our menu-type components, including `DropdownMenu` and `ActionMenu`. -## Default example +## Minimal example ```jsx live ``` +## Example with grouped items + +```jsx live + +``` + ## Component props | Name | Type | Default | Description | From 1008dce035419634f413e4edc99191dcfc113030 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Wed, 31 Mar 2021 13:08:06 -0400 Subject: [PATCH 24/31] fix: Adopt functional color variables. Replace 'bg.gray' and 'border.gray' with 'bg.tertiary' and 'border.tertiary'. --- src/ActionList/Header.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ActionList/Header.tsx b/src/ActionList/Header.tsx index 8e73dec6bc2..6e59ce99ee4 100644 --- a/src/ActionList/Header.tsx +++ b/src/ActionList/Header.tsx @@ -34,10 +34,10 @@ const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` ${({variant}) => variant === 'filled' && css` - background: ${get('colors.bg.gray')}; + background: ${get('colors.bg.tertiary')}; margin: ${get('space.2')} -${get('space.2')}; - border-top: 1px solid ${get('colors.border.gray')}; - border-bottom: 1px solid ${get('colors.border.gray')}; + border-top: 1px solid ${get('colors.border.tertiary')}; + border-bottom: 1px solid ${get('colors.border.tertiary')}; &:first-child { margin-top: 0; From fa98c7cbf7dd4270dbb9fe88ca79996d3ee76709 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Wed, 31 Mar 2021 13:13:41 -0400 Subject: [PATCH 25/31] fix: Run Prettier --- src/ActionList/Item.tsx | 4 ++-- src/ActionList/List.tsx | 1 - src/utils/types.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 05f3df7aac3..02493cfc9f1 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -19,7 +19,7 @@ interface ItemPropsBase extends React.ComponentPropsWithoutRef<'div'> { /** * Secondary text style variations. Usage is discretionary. - * + * * - `"inline"` - Secondary text is positioned beside primary text. * - `"block"` - Secondary text is positioned below primary text. */ @@ -32,7 +32,7 @@ interface ItemPropsBase extends React.ComponentPropsWithoutRef<'div'> { /** * Style variations associated with various `Item` types. - * + * * - `"default"` - An action `Item`. * - `"danger"` - A destructive action `Item`. */ diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index 9ac38dfcc7b..c55adcf80a4 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -83,7 +83,6 @@ 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(toJSX)}] - } else { // When `groupMetadata` is provided, collect rendered `Item`s into their associated `Group`s. diff --git a/src/utils/types.ts b/src/utils/types.ts index 9d317393551..b68feb8c60e 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -15,4 +15,4 @@ export type ComponentProps = T extends React.ComponentType /** * Contruct a type describing the items in `T`, if `T` is an array. */ -export type Flatten = T extends (infer U)[] ? U : never \ No newline at end of file +export type Flatten = T extends (infer U)[] ? U : never From 7e98f6b7bf6adef464f622d38428234ccb876ce8 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Wed, 31 Mar 2021 13:33:52 -0400 Subject: [PATCH 26/31] fix: Hardcode 4px-based space values where it increases readability --- src/ActionList/Header.tsx | 9 ++++++++- src/ActionList/Item.tsx | 35 ++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/ActionList/Header.tsx b/src/ActionList/Header.tsx index 6e59ce99ee4..3755c40ff62 100644 --- a/src/ActionList/Header.tsx +++ b/src/ActionList/Header.tsx @@ -26,7 +26,14 @@ export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'> { } const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` - padding: calc((${get('space.3')} - ${get('space.1')}) / 2) ${get('space.2')}; + { + /* 6px vertical padding + 20px line height = 32px total height + * + * TODO: When rem-based spacing on a 4px scale lands, replace + * hardcoded '6px' with 'calc((${get('space.s32')} - ${get('space.20')}) / 2)'. + */ + } + padding: 6px ${get('space.2')}; font-size: ${get('fontSizes.0')}; font-weight: ${get('fontWeights.bold')}; color: ${get('colors.text.secondary')}; diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 02493cfc9f1..bfdfabaf566 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -55,17 +55,23 @@ interface ItemPropsWithRenderItem extends Partial { export type ItemProps = ItemPropsBase | ItemPropsWithRenderItem const StyledItem = styled.div<{variant: ItemProps['variant']}>` - display: flex; - border-radius: ${get('radii.2')}; - color: ${({variant}) => (variant === 'danger' ? get('colors.text.danger') : 'inherit')}; - padding: calc((${get('space.3')} - ${get('space.1')}) / 2) ${get('space.2')}; + { + /* 6px vertical padding + 20px line height = 32px total height + * + * TODO: When rem-based spacing on a 4px scale lands, replace + * hardcoded '6px' with 'calc((${get('space.s32')} - ${get('space.20')}) / 2)'. + */ + } + padding: 6px ${get('space.2')}; + display: flex; + border-radius: ${get('radii.2')}; + color: ${({variant}) => (variant === 'danger' ? get('colors.text.danger') : 'inherit')}; - @media (hover: hover) and (pointer: fine) { - :hover { - background: ${props => - props.variant === 'danger' ? get('colors.bg.danger') : get('colors.selectMenu.tapHighlight')}; - cursor: pointer; - } + @media (hover: hover) and (pointer: fine) { + :hover { + background: ${props => + props.variant === 'danger' ? get('colors.bg.danger') : get('colors.selectMenu.tapHighlight')}; + cursor: pointer; } } ` @@ -75,8 +81,15 @@ const StyledTextContainer = styled.div<{descriptionVariant: ItemProps['descripti ` const LeadingVisualContainer = styled.div` + { + /* Match visual height to adjacent text line height. + * + * TODO: When rem-based spacing on a 4px scale lands, replace + * hardcoded '20px' with '${get('space.s20')}'. + */ + } + height: 20px; width: ${get('space.3')}; - height: calc(${get('space.4')} - ${get('space.1')}); display: flex; flex-direction: column; justify-content: center; From d70271b8beea674305fa8359de6c68f6742d9d05 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Wed, 31 Mar 2021 13:41:27 -0400 Subject: [PATCH 27/31] fix: Remove negative margin in 'Divider'; instead, apply spacing to 'List', 'Header' and 'Item' to vertically and horizontally indent them --- src/ActionList/Divider.tsx | 3 ++- src/ActionList/Header.tsx | 2 ++ src/ActionList/Item.tsx | 2 ++ src/ActionList/List.tsx | 2 ++ src/stories/ActionList.stories.tsx | 1 - 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ActionList/Divider.tsx b/src/ActionList/Divider.tsx index fbe37111a78..c872a589f43 100644 --- a/src/ActionList/Divider.tsx +++ b/src/ActionList/Divider.tsx @@ -5,7 +5,8 @@ import {get} from '../constants' const StyledDivider = styled.div` height: 1px; background: ${get('colors.selectMenu.borderSecondary')}; - margin: calc(${get('space.2')} - 1px) -${get('space.2')} ${get('space.2')}; + margin-top: calc(${get('space.2')} - 1px); + margin-bottom: ${get('space.2')}; ` /** diff --git a/src/ActionList/Header.tsx b/src/ActionList/Header.tsx index 3755c40ff62..87b06ce2eda 100644 --- a/src/ActionList/Header.tsx +++ b/src/ActionList/Header.tsx @@ -34,6 +34,8 @@ const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` */ } padding: 6px ${get('space.2')}; + margin-left: ${get('space.2')}; + margin-right: ${get('space.2')}; font-size: ${get('fontSizes.0')}; font-weight: ${get('fontWeights.bold')}; color: ${get('colors.text.secondary')}; diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index bfdfabaf566..6388fad9474 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -63,6 +63,8 @@ const StyledItem = styled.div<{variant: ItemProps['variant']}>` */ } padding: 6px ${get('space.2')}; + margin-left: ${get('space.2')}; + margin-right: ${get('space.2')}; display: flex; border-radius: ${get('radii.2')}; color: ${({variant}) => (variant === 'danger' ? get('colors.text.danger') : 'inherit')}; diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index c55adcf80a4..aa62453e4d0 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -59,6 +59,8 @@ type GroupWithItems = Omit, 'groupId' const StyledList = styled.div` font-size: ${get('fontSizes.1')}; + padding-top: ${get('space.2')}; + padding-bottom: ${get('space.2')}; ` /** diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index e380d16056c..19ff7f68a52 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -44,7 +44,6 @@ export default meta const ErsatzOverlay = styled.div` border-radius: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 8px 24px rgba(149, 157, 165, 0.2); - padding: 8px; ` export function ActionsStory(): JSX.Element { From 1e44f5c64fdcfb3f75a6ce280fd5a43c6d7fee5d Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Wed, 31 Mar 2021 14:58:08 -0400 Subject: [PATCH 28/31] fix: Remove negative margin in 'filled' 'Header' to left-align background. Add padding to align inner text --- src/ActionList/Header.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ActionList/Header.tsx b/src/ActionList/Header.tsx index 87b06ce2eda..73692815524 100644 --- a/src/ActionList/Header.tsx +++ b/src/ActionList/Header.tsx @@ -44,7 +44,9 @@ const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` variant === 'filled' && css` background: ${get('colors.bg.tertiary')}; - margin: ${get('space.2')} -${get('space.2')}; + margin: ${get('space.2')} 0; + padding-left: ${get('space.3')}; + padding-right: ${get('space.3')}; border-top: 1px solid ${get('colors.border.tertiary')}; border-bottom: 1px solid ${get('colors.border.tertiary')}; From b92bc6bb83d07c4c5a88f49b57b6bb0440a538e2 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Wed, 31 Mar 2021 16:03:28 -0400 Subject: [PATCH 29/31] fix: Update 'ActionList' snapshot --- src/__tests__/__snapshots__/ActionList.tsx.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/__tests__/__snapshots__/ActionList.tsx.snap b/src/__tests__/__snapshots__/ActionList.tsx.snap index 60323046e0f..84eab938692 100644 --- a/src/__tests__/__snapshots__/ActionList.tsx.snap +++ b/src/__tests__/__snapshots__/ActionList.tsx.snap @@ -3,6 +3,8 @@ exports[`ActionList renders consistently 1`] = ` .c0 { font-size: 14px; + padding-top: 8px; + padding-bottom: 8px; }
Date: Thu, 1 Apr 2021 17:25:23 -0400 Subject: [PATCH 30/31] feat: Refactor to support 'renderGroup' --- src/ActionList/Divider.tsx | 2 +- src/ActionList/Group.tsx | 30 +++++-- src/ActionList/Header.tsx | 23 +++-- src/ActionList/Item.tsx | 36 +++----- src/ActionList/List.tsx | 131 ++++++++++++++++++++++------- src/stories/ActionList.stories.tsx | 85 ++++++++++++++++++- 6 files changed, 229 insertions(+), 78 deletions(-) diff --git a/src/ActionList/Divider.tsx b/src/ActionList/Divider.tsx index c872a589f43..1914e82b985 100644 --- a/src/ActionList/Divider.tsx +++ b/src/ActionList/Divider.tsx @@ -17,7 +17,7 @@ export function Divider(): JSX.Element { } /** - * `Divider` fulfills the `ItemPropsWithRenderItem` contract, + * `Divider` fulfills the `ItemPropsWithCustomRenderer` contract, * so it can be used inline in an `ActionList`’s `items` prop. * In other words, `items={[ActionList.Divider]}` is supported as a concise * alternative to `items={[{renderItem: () => }]}`. diff --git a/src/ActionList/Group.tsx b/src/ActionList/Group.tsx index a4ea3ab599f..4fb87ad5bc7 100644 --- a/src/ActionList/Group.tsx +++ b/src/ActionList/Group.tsx @@ -1,21 +1,35 @@ import React from 'react' -import {ItemProps} from './Item' +import styled from 'styled-components' +import sx, {SxProp} from '../sx' +import {Header, HeaderProps} from './Header' /** * Contract for props passed to the `Group` component. */ -export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> { +export interface GroupProps extends React.ComponentPropsWithoutRef<'div'>, SxProp { /** - * A `Group`-level custom `Item` renderer. Every `Item` within this `Group` - * without an `Item`-level custom `Item` renderer will be rendered using - * this function component. + * Props for a `Header` to render in the `Group`. */ - renderItem?: (props: ItemProps) => JSX.Element + header?: HeaderProps + + /** + * `Items` to render in the `Group`. + */ + items?: JSX.Element[] } +const StyledGroup = styled.div` + ${sx} +` + /** * Collects related `Items` in an `ActionList`. */ -export function Group({renderItem: _renderItem, ...props}: GroupProps): JSX.Element { - return
+export function Group({header, items, ...props}: GroupProps): JSX.Element { + return ( + + {header &&
} + {items} + + ) } diff --git a/src/ActionList/Header.tsx b/src/ActionList/Header.tsx index 73692815524..a4726d5f9a7 100644 --- a/src/ActionList/Header.tsx +++ b/src/ActionList/Header.tsx @@ -1,18 +1,19 @@ import React from 'react' import styled, {css} from 'styled-components' import {get} from '../constants' +import sx, {SxProp} from '../sx' /** * Contract for props passed to the `Header` component. */ -export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'> { +export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'>, SxProp { /** * Style variations. Usage is discretionary. * * - `"filled"` - Superimposed on a background, offset from nearby content * - `"subtle"` - Relatively less offset from nearby content */ - variant: 'subtle' | 'filled' + variant?: 'subtle' | 'filled' /** * Primary text which names a `Group`. @@ -25,7 +26,7 @@ export interface HeaderProps extends React.ComponentPropsWithoutRef<'div'> { auxiliaryText?: string } -const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` +const StyledHeader = styled.div<{variant: HeaderProps['variant']} & SxProp>` { /* 6px vertical padding + 20px line height = 32px total height * @@ -33,9 +34,7 @@ const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` * hardcoded '6px' with 'calc((${get('space.s32')} - ${get('space.20')}) / 2)'. */ } - padding: 6px ${get('space.2')}; - margin-left: ${get('space.2')}; - margin-right: ${get('space.2')}; + padding: 6px ${get('space.3')}; font-size: ${get('fontSizes.0')}; font-weight: ${get('fontWeights.bold')}; color: ${get('colors.text.secondary')}; @@ -45,8 +44,6 @@ const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` css` background: ${get('colors.bg.tertiary')}; margin: ${get('space.2')} 0; - padding-left: ${get('space.3')}; - padding-right: ${get('space.3')}; border-top: 1px solid ${get('colors.border.tertiary')}; border-bottom: 1px solid ${get('colors.border.tertiary')}; @@ -54,12 +51,20 @@ const StyledHeader = styled.div<{variant: HeaderProps['variant']}>` margin-top: 0; } `} + + ${sx} ` /** * Displays the name and description of a `Group`. */ -export function Header({variant, title, auxiliaryText, children: _children, ...props}: HeaderProps): JSX.Element { +export function Header({ + variant = 'subtle', + title, + auxiliaryText, + children: _children, + ...props +}: HeaderProps): JSX.Element { return ( {title} diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 6388fad9474..d22996a5bcb 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -2,11 +2,12 @@ import type {IconProps} from '@primer/octicons-react' import React from 'react' import styled from 'styled-components' import {get} from '../constants' +import sx, {SxProp} from '../sx' /** * Contract for props passed to the `Item` component. */ -interface ItemPropsBase extends React.ComponentPropsWithoutRef<'div'> { +export interface ItemProps extends React.ComponentPropsWithoutRef<'div'>, SxProp { /** * Primary text which names an `Item`. */ @@ -39,32 +40,13 @@ interface ItemPropsBase extends React.ComponentPropsWithoutRef<'div'> { variant?: 'default' | 'danger' } -/** - * Contract for props passed to the `Item` component, when an `Item`-level custom `Item` renderer is used. - */ -interface ItemPropsWithRenderItem extends Partial { - /** - * An `Item`-level custom `Item` renderer. +const StyledItem = styled.div<{variant: ItemProps['variant']} & SxProp>` + /* 6px vertical padding + 20px line height = 32px total height + * + * TODO: When rem-based spacing on a 4px scale lands, replace + * hardcoded '6px' with 'calc((${get('space.s32')} - ${get('space.20')}) / 2)'. */ - renderItem: (props: ItemProps) => JSX.Element -} - -/** - * Contract for props passed to the `Item` component. - */ -export type ItemProps = ItemPropsBase | ItemPropsWithRenderItem - -const StyledItem = styled.div<{variant: ItemProps['variant']}>` - { - /* 6px vertical padding + 20px line height = 32px total height - * - * TODO: When rem-based spacing on a 4px scale lands, replace - * hardcoded '6px' with 'calc((${get('space.s32')} - ${get('space.20')}) / 2)'. - */ - } padding: 6px ${get('space.2')}; - margin-left: ${get('space.2')}; - margin-right: ${get('space.2')}; display: flex; border-radius: ${get('radii.2')}; color: ${({variant}) => (variant === 'danger' ? get('colors.text.danger') : 'inherit')}; @@ -76,6 +58,8 @@ const StyledItem = styled.div<{variant: ItemProps['variant']}>` cursor: pointer; } } + + ${sx} ` const StyledTextContainer = styled.div<{descriptionVariant: ItemProps['descriptionVariant']}>` @@ -117,7 +101,7 @@ export function Item({ leadingVisual: LeadingVisual, variant = 'default', ...props -}: ItemProps): JSX.Element { +}: Partial): JSX.Element { return ( {LeadingVisual && ( diff --git a/src/ActionList/List.tsx b/src/ActionList/List.tsx index aa62453e4d0..8038cbc9950 100644 --- a/src/ActionList/List.tsx +++ b/src/ActionList/List.tsx @@ -2,19 +2,18 @@ import {Group, GroupProps} from './Group' import {Item, ItemProps} from './Item' import React from 'react' import {Divider} from './Divider' -import {Header, HeaderProps} from './Header' import styled from 'styled-components' import {get} from '../constants' -import type {Flatten} from '../utils/types' +import {SystemCssProperties} from '@styled-system/css' /** * Contract for props passed to the `List` component. */ -interface UngroupedListProps { +interface ListPropsBase { /** - * `Item`s to render in the `List`. + * A collection of `Item` props and `Item`-level custom `Item` renderers. */ - items: ItemProps[] + items: (ItemProps | (Partial & {renderItem: typeof Item}))[] /** * A `List`-level custom `Item` renderer. Every `Item` within this `List` @@ -22,26 +21,46 @@ interface UngroupedListProps { * rendered using this function component. */ renderItem?: (props: ItemProps) => JSX.Element + + /** + * A `List`-level custom `Group` renderer. Every `Group` within this `List` + * without a `Group`-level custom `Item` renderer will be rendered using + * this function component. + */ + renderGroup?: (props: GroupProps) => JSX.Element + + /** + * Style variations. Usage is discretionary. + * + * - `"inset"` - `List` children are offset (vertically and horizontally) from `List`’s edges + * - `"full"` - `List` children are flush (vertically and horizontally) with `List` edges + */ + variant?: 'inset' | 'full' } /** * Contract for props passed to the `List` component, when its `Item`s are collected in `Group`s. */ -interface GroupedListProps extends UngroupedListProps { +interface GroupedListProps extends ListPropsBase { /** - * An array of `Group`s, each an associated `Header` and a group idenitifier. + * A collection of `Group` props (except `items`), plus a unique group identifier + * and `Group`-level custom `Item` or `Group` renderers. */ - groupMetadata: (GroupProps & {groupId: string; header?: HeaderProps})[] + groupMetadata: (( + | Omit + | Omit & {renderItem?: typeof Item; renderGroup?: typeof Group}, 'items'> + ) & {groupId: string})[] /** - * `Items` to render in the `List`, each with a group identifier used to associate it with its `Group`. + * A collection of `Item` props, plus associated group identifiers + * and `Item`-level custom `Item` renderers. */ - items: (ItemProps & {groupId: string})[] + items: ((ItemProps | (Partial & {renderItem: typeof Item})) & {groupId: string})[] } /** * Asserts that the given value fulfills the `GroupedListProps` contract. - * @param props A value which fulfills either the `UngroupedListProps` or the `GroupedListProps` contract. + * @param props A value which fulfills either the `ListPropsBase` or the `GroupedListProps` contract. */ function isGroupedListProps(props: ListProps): props is GroupedListProps { return 'groupMetadata' in props @@ -50,50 +69,85 @@ function isGroupedListProps(props: ListProps): props is GroupedListProps { /** * Contract for props passed to the `List` component. */ -export type ListProps = UngroupedListProps | GroupedListProps - -/** - * An array of `Group`s, each with an associated `Header` and with an array of `Item`s belonging to that `Group`. - */ -type GroupWithItems = Omit, 'groupId'> & {items?: JSX.Element[]} +export type ListProps = ListPropsBase | GroupedListProps const StyledList = styled.div` font-size: ${get('fontSizes.1')}; - padding-top: ${get('space.2')}; - padding-bottom: ${get('space.2')}; ` /** - * Lists `Item`s, either grouped or ungrouped, with a `Divider` between each group. + * Returns `sx` prop values for `List` children matching the given `List` style variation. + * @param variant `List` style variation. + */ +function useListVariant( + variant: ListProps['variant'] = 'inset' +): { + firstGroupStyle?: SystemCssProperties + lastGroupStyle?: SystemCssProperties + headerStyle?: SystemCssProperties + itemStyle?: SystemCssProperties +} { + switch (variant) { + case 'full': + return { + headerStyle: {paddingX: get('space.2')}, + itemStyle: {borderRadius: 0} + } + default: + return { + firstGroupStyle: {marginTop: get('space.2')}, + lastGroupStyle: {marginBottom: get('space.2')}, + itemStyle: {marginX: get('space.2')} + } + } +} + +/** + * Lists `Item`s, either grouped or ungrouped, with a `Divider` between each `Group`. */ export function List(props: ListProps): JSX.Element { + // Get `sx` prop values for `List` children matching the given `List` style variation. + const {firstGroupStyle, lastGroupStyle, headerStyle, itemStyle} = useListVariant(props.variant) + + /** + * Render a `Group` using the first of the following renderers that is defined: + * A `Group`-level or `List`-level custom `Group` renderer, or + * the default `Group` renderer. + */ + const renderGroup = ( + groupProps: GroupProps | (Partial & {renderItem?: typeof Item; renderGroup?: typeof Group}) + ) => ((('renderGroup' in groupProps && groupProps.renderGroup) ?? props.renderGroup) || Group).call(null, groupProps) + /** * Render an `Item` using the first of the following renderers that is defined: * An `Item`-level, `Group`-level, or `List`-level custom `Item` renderer, * or the default `Item` renderer. */ - const toJSX = (itemProps: ItemProps) => - ((('renderItem' in itemProps && itemProps.renderItem) ?? props.renderItem) || Item).call(null, itemProps) + const renderItem = (itemProps: ItemProps | (Partial & {renderItem: typeof Item})) => + ((('renderItem' in itemProps && itemProps.renderItem) ?? props.renderItem) || Item).call(null, { + ...itemProps, + sx: {...itemStyle, ...itemProps.sx} + }) /** * An array of `Group`s, each with an associated `Header` and with an array of `Item`s belonging to that `Group`. */ - let groups: GroupWithItems[] = [] + let groups: (GroupProps | (Partial & {renderItem?: typeof Item; renderGroup?: typeof Group}))[] = [] // Collect rendered `Item`s into `Group`s, avoiding excess iteration over the lists of `items` and `groupMetadata`: if (!isGroupedListProps(props)) { // When no `groupMetadata`s is provided, collect rendered `Item`s into a single anonymous `Group`. - groups = [{items: props.items?.map(toJSX)}] + groups = [{items: props.items?.map(renderItem)}] } else { // When `groupMetadata` is provided, collect rendered `Item`s into their associated `Group`s. /** - * A map of group identifiers to `Group`s, each with an associated `Header` and an array of `Item`s belonging to that `Group`. + * A map of group identifiers to `Group`s, each with an associated array of `Item`s belonging to that `Group`. */ const groupMap = props.groupMetadata.reduce( (groups, groupMetadata) => groups.set(groupMetadata.groupId, groupMetadata), - new Map() + new Map & {renderItem?: typeof Item; renderGroup?: typeof Group})>() ) for (const itemProps of props.items) { @@ -103,7 +157,10 @@ export function List(props: ListProps): JSX.Element { // Upsert the group to include the current item (rendered). groupMap.set(itemProps.groupId, { ...group, - items: [...(group?.items ?? []), toJSX({renderItem: group?.renderItem ?? props.renderItem, ...itemProps})] + items: [ + ...(group?.items ?? []), + renderItem({...(group && 'renderItem' in group && {renderItem: group.renderItem}), ...itemProps}) + ] }) } @@ -112,12 +169,22 @@ export function List(props: ListProps): JSX.Element { return ( - {groups?.map(({header, items}, index) => ( + {groups?.map(({header, ...groupProps}, index) => ( <> - - {header &&
} - {items} - + {renderGroup({ + key: index, + sx: { + ...(index === 0 && firstGroupStyle), + ...(index === groups.length - 1 && lastGroupStyle) + }, + ...(header && { + header: { + ...header, + sx: {...headerStyle, ...header?.sx} + } + }), + ...groupProps + })} {index + 1 !== groups.length && } ))} diff --git a/src/stories/ActionList.stories.tsx b/src/stories/ActionList.stories.tsx index 19ff7f68a52..40d0eb2a1ea 100644 --- a/src/stories/ActionList.stories.tsx +++ b/src/stories/ActionList.stories.tsx @@ -16,6 +16,7 @@ import {ThemeProvider} from '..' import {ActionList as _ActionList} from '../ActionList' import {Header} from '../ActionList/Header' import BaseStyles from '../BaseStyles' +import sx from '../sx' const ActionList = Object.assign(_ActionList, { Header @@ -44,6 +45,7 @@ export default meta const ErsatzOverlay = styled.div` border-radius: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 8px 24px rgba(149, 157, 165, 0.2); + overflow: hidden; ` export function ActionsStory(): JSX.Element { @@ -95,17 +97,96 @@ export function SimpleListStory(): JSX.Element { SimpleListStory.storyName = 'Simple List' export function ComplexListStory(): JSX.Element { + const StyledDiv = styled.div` + ${sx} + ` return ( <>

Complex List

+

Inset Variant

}, - {groupId: '4'} + { + groupId: '4', + renderItem: ({leadingVisual: LeadingVisual, ...props}) => ( + ( + svg': {fill: 'white'}}}> + {LeadingVisual && } + + )} + /> + ), + renderGroup: ({sx, ...props}) => ( + + ) + } + ]} + items={[ + {leadingVisual: TypographyIcon, text: 'Rename', groupId: '0'}, + {leadingVisual: VersionsIcon, text: 'Duplicate', groupId: '0'}, + { + leadingVisual: SearchIcon, + text: 'repo:github/memex,github/github', + groupId: '1', + renderItem: props => + }, + { + leadingVisual: NoteIcon, + text: 'Table', + description: 'Information-dense table optimized for operations across teams', + descriptionVariant: 'block', + groupId: '2' + }, + { + leadingVisual: ProjectIcon, + text: 'Board', + description: 'Kanban-style board focused on visual states', + descriptionVariant: 'block', + groupId: '2' + }, + { + leadingVisual: FilterIcon, + text: 'Save sort and filters to current view', + groupId: '3' + }, + {leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: '3'}, + {leadingVisual: GearIcon, text: 'View settings', groupId: '4'} + ]} + /> + + +

Full Variant

+ + }, + { + groupId: '4', + renderItem: ({leadingVisual: LeadingVisual, ...props}) => ( + ( + svg': {fill: 'white'}}}> + {LeadingVisual && } + + )} + /> + ), + renderGroup: ({sx, ...props}) => ( + + ) + } ]} items={[ {leadingVisual: TypographyIcon, text: 'Rename', groupId: '0'}, From 234b777bf56f5723c2aeeb006d89e2d46bb77638 Mon Sep 17 00:00:00 2001 From: Clay Miller Date: Thu, 1 Apr 2021 21:41:09 -0400 Subject: [PATCH 31/31] fix: Update 'ActionList' snapshot to support 'inset' and 'full' style variation CSS --- src/__tests__/__snapshots__/ActionList.tsx.snap | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/__tests__/__snapshots__/ActionList.tsx.snap b/src/__tests__/__snapshots__/ActionList.tsx.snap index 84eab938692..b33d7ef971e 100644 --- a/src/__tests__/__snapshots__/ActionList.tsx.snap +++ b/src/__tests__/__snapshots__/ActionList.tsx.snap @@ -1,15 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ActionList renders consistently 1`] = ` +.c1 { + margin-top: 8px; + margin-bottom: 8px; +} + .c0 { font-size: 14px; - padding-top: 8px; - padding-bottom: 8px; }
-
+
`;