diff --git a/.github/workflows/verify-labels.yml b/.github/workflows/verify-labels.yml index 6049aaec..cf171141 100644 --- a/.github/workflows/verify-labels.yml +++ b/.github/workflows/verify-labels.yml @@ -1,18 +1,22 @@ -name: Verify Labels - +name: Pull Request Labels on: pull_request: types: [opened, labeled, unlabeled, synchronize] - jobs: check_pr_labels: + name: Verify that the PR has the appropriate label(s) runs-on: ubuntu-latest - name: Verify that the PR has a valid label steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Verify PR label action - uses: jesusvasquez333/verify-pr-label-action@v1.1.0 + - name: Check for version label + if: ${{ github.base_ref == 'master' }} + uses: mheap/github-action-required-labels@v1 with: - github-token: '${{ secrets.GITHUB_TOKEN }}' - valid-labels: 'fix, bug, bugfix, feature, enhancement, chore' + mode: exactly + count: 1 + labels: 'patch, minor, major' + - name: Check for issue type label + uses: mheap/github-action-required-labels@v1 + with: + mode: minimum + count: 1 + labels: 'fix, bug, bugfix, feature, enhancement, chore' diff --git a/src/__snapshots__/storybook.test.ts.snap b/src/__snapshots__/storybook.test.ts.snap index 2f9fd953..bbab594d 100644 --- a/src/__snapshots__/storybook.test.ts.snap +++ b/src/__snapshots__/storybook.test.ts.snap @@ -85,6 +85,22 @@ exports[`Storyshots Button Primary Disabled 1`] = ` `; +exports[`Storyshots Icon Custom 1`] = ` +https://dummyimage.com/600x400/000/fff&text=Dassana +`; + +exports[`Storyshots Icon Predefined 1`] = ` +dassana-orange +`; + exports[`Storyshots InputField Default 1`] = `
`; -exports[`Storyshots Link Default 1`] = ` +exports[`Storyshots Link Click 1`] = ` - Default + Click + + +`; + +exports[`Storyshots Link Href 1`] = ` + + + Href `; @@ -249,3 +281,59 @@ exports[`Storyshots Tag Default 1`] = ` Default `; + +exports[`Storyshots Toggle Checked Disabled 1`] = ` + +`; + +exports[`Storyshots Toggle Default 1`] = ` + +`; + +exports[`Storyshots Toggle Disabled 1`] = ` + +`; diff --git a/src/assets/icons/aws.svg b/src/assets/icons/aws.svg new file mode 100644 index 00000000..df5331c9 --- /dev/null +++ b/src/assets/icons/aws.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + diff --git a/src/assets/icons/dassana-blue.png b/src/assets/icons/dassana-blue.png new file mode 100644 index 00000000..fe5b0b1c Binary files /dev/null and b/src/assets/icons/dassana-blue.png differ diff --git a/src/assets/icons/dassana-orange.png b/src/assets/icons/dassana-orange.png new file mode 100644 index 00000000..4c268b12 Binary files /dev/null and b/src/assets/icons/dassana-orange.png differ diff --git a/src/components/Button/Button.test.tsx b/src/components/Button/Button.test.tsx index 98728d10..e5774b71 100644 --- a/src/components/Button/Button.test.tsx +++ b/src/components/Button/Button.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import Button, { ButtonProps } from './index' +import Button, { ButtonProps } from '.' import { shallow, ShallowWrapper } from 'enzyme' let wrapper: ShallowWrapper diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 8d82328e..badeeae6 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -19,7 +19,7 @@ export interface ButtonProps { primary?: boolean /** * Adds the disabled attribute and styles (opacity, gray scale filter, no pointer events). - * */ + */ disabled?: boolean /** * Array of classes to pass to button. diff --git a/src/components/Icon/Icon.stories.tsx b/src/components/Icon/Icon.stories.tsx new file mode 100644 index 00000000..f4d5286d --- /dev/null +++ b/src/components/Icon/Icon.stories.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import Icon, { IconProps } from '.' +import { Meta, Story } from '@storybook/react/types-6-0' + +export default { + argTypes: { + height: { defaultValue: 64 } + }, + component: Icon, + title: 'Icon' +} as Meta + +const Template: Story = args => + +export const Predefined = Template.bind({}) +Predefined.argTypes = { + icon: { + control: { disable: true } + } +} +Predefined.args = { + iconKey: 'dassana-orange' +} + +export const Custom = Template.bind({}) +Custom.argTypes = { + iconKey: { + control: { disable: true } + } +} +Custom.args = { + icon: 'https://dummyimage.com/600x400/000/fff&text=Dassana' +} diff --git a/src/components/Icon/Icon.test.tsx b/src/components/Icon/Icon.test.tsx new file mode 100644 index 00000000..ea654ad0 --- /dev/null +++ b/src/components/Icon/Icon.test.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import Icon, { IconProps } from '.' +import { mount, ReactWrapper } from 'enzyme' + +let wrapper: ReactWrapper + +describe('Predefined Icon', () => { + const mockProps: IconProps = { + height: 64, + iconKey: 'dassana-blue' + } + + beforeEach(() => { + wrapper = mount() + }) + + it('renders', () => { + expect(wrapper).toHaveLength(1) + }) + + it('renders with correct src url', () => { + expect(wrapper.getDOMNode().getAttribute('src')).toContain( + 'dassana-blue' + ) + }) + + it('has the correct alt attribute', () => { + expect(wrapper.getDOMNode().getAttribute('alt')).toBe('dassana-blue') + }) + + it('has the correct height', () => { + expect(wrapper.getDOMNode().getAttribute('height')).toBe('64') + }) +}) + +describe('Custom Icon', () => { + const mockProps: IconProps = { + height: 64, + icon: 'https://dummyimage.com/600x400/000/fff&text=Dassana' + } + + beforeEach(() => { + wrapper = mount() + }) + + it('renders', () => { + expect(wrapper).toHaveLength(1) + }) + + it('renders with correct src url', () => { + expect(wrapper.getDOMNode().getAttribute('src')).toBe( + 'https://dummyimage.com/600x400/000/fff&text=Dassana' + ) + }) + + it('has the correct alt attribute', () => { + expect(wrapper.getDOMNode().getAttribute('alt')).toBe( + 'https://dummyimage.com/600x400/000/fff&text=Dassana' + ) + }) + + it('has the correct height', () => { + expect(wrapper.getDOMNode().getAttribute('height')).toBe('64') + }) +}) diff --git a/src/components/Icon/IconsMap.ts b/src/components/Icon/IconsMap.ts new file mode 100644 index 00000000..3361e0ff --- /dev/null +++ b/src/components/Icon/IconsMap.ts @@ -0,0 +1,13 @@ +import AWS from 'assets/icons/aws.svg' +import DASSANA_BLUE from 'assets/icons/dassana-blue.png' +import DASSANA_ORANGE from 'assets/icons/dassana-orange.png' + +const Icons = { + 'aws-logo': AWS, + 'dassana-blue': DASSANA_BLUE, + 'dassana-orange': DASSANA_ORANGE +} + +export type IconName = keyof typeof Icons + +export default Icons diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx new file mode 100644 index 00000000..705c78f3 --- /dev/null +++ b/src/components/Icon/index.tsx @@ -0,0 +1,41 @@ +import Icons, { IconName } from './IconsMap' +import React, { FC } from 'react' + +export type { IconName } + +interface SharedIconProps { + /** + * The height of the icon, in pixels. Width will be calculated by default. + */ + height?: number +} + +interface IconPath extends SharedIconProps { + /** + * The url of the icon if rendering a custom icon. + */ + icon: string + /** + * The name of the icon if using icons provided by Dassana. **Note**: Either an `icon` or `iconKey` is required. + */ + iconKey?: never +} + +interface IconKey extends SharedIconProps { + iconKey: IconName + icon?: never +} + +export type IconProps = IconKey | IconPath + +const Icon: FC = ({ height = 32, ...props }: IconProps) => { + const { icon } = props as IconPath + const { iconKey } = props as IconKey + + const imgSrc = iconKey ? Icons[iconKey] : icon + const imgAlt = iconKey ? iconKey : icon + + return {imgAlt} +} + +export default Icon diff --git a/src/components/Link/Link.stories.tsx b/src/components/Link/Link.stories.tsx index c0712493..3a103800 100644 --- a/src/components/Link/Link.stories.tsx +++ b/src/components/Link/Link.stories.tsx @@ -1,6 +1,6 @@ import { action } from '@storybook/addon-actions' import React from 'react' -import Link, { LinkProps } from './index' +import Link, { LinkProps } from '.' import { Meta, Story } from '@storybook/react/types-6-0' export default { @@ -11,13 +11,26 @@ export default { title: 'Link' } as Meta -const linkProps: LinkProps = { - children: 'Default', - href: ' ', - onClick: action('onClick') -} - const Template: Story = args => -export const Default = Template.bind({}) -Default.args = linkProps +export const Href = Template.bind({}) +Href.argTypes = { + onClick: { + control: { disable: true } + } +} +Href.args = { + children: 'Href', + href: ' ' +} + +export const Click = Template.bind({}) +Click.argTypes = { + href: { + control: { disable: true } + } +} +Click.args = { + children: 'Click', + onClick: action('onClick') +} diff --git a/src/components/Link/Link.test.tsx b/src/components/Link/Link.test.tsx index 504d33f1..b2ea2514 100644 --- a/src/components/Link/Link.test.tsx +++ b/src/components/Link/Link.test.tsx @@ -1,43 +1,62 @@ import React from 'react' import Link, { LinkProps } from '.' -import { mount, ReactWrapper, shallow } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' let wrapper: ReactWrapper let mockClick: jest.Mock -const mockProps: LinkProps = { - children: 'Test', - href: '/test', - target: '_blank' -} - -beforeEach(() => { - mockClick = jest.fn() - wrapper = mount() -}) +let mockProps: LinkProps -describe('Link', () => { - it('renders', () => { - const link = wrapper.find(Link) - expect(link).toHaveLength(1) +describe('Link with href', () => { + beforeEach(() => { + mockProps = { + children: 'Test', + href: '/test', + target: '_blank' + } + + wrapper = mount() }) - it('calls onClick function when link is clicked', () => { - const link = wrapper.find(Link) - link.simulate('click') - expect(mockClick).toHaveBeenCalledTimes(1) + it('renders', () => { + expect(wrapper).toHaveLength(1) }) it('has the correct href attribute', () => { - const link = wrapper.find(Link) - expect(link.getDOMNode().getAttribute('href')).toBe(mockProps.href) + expect(wrapper.getDOMNode().getAttribute('href')).toBe(mockProps.href) }) it('has the correct target attribute', () => { - const link = wrapper.find(Link) - expect(link.getDOMNode().getAttribute('target')).toBe(mockProps.target) + expect(wrapper.getDOMNode().getAttribute('target')).toBe( + mockProps.target + ) }) +}) + +describe('Link', () => { + beforeEach(() => { + mockClick = jest.fn() + + mockProps = { + children: 'Test', + onClick: mockClick, + target: '_blank' + } - it('throws an error if both onClick and href props are undefined', () => { - expect(() => shallow(Test)).toThrow() + wrapper = mount() + }) + + it('renders', () => { + expect(wrapper).toHaveLength(1) + }) + + it('calls onClick function when link is clicked', () => { + wrapper.simulate('click') + expect(mockClick).toHaveBeenCalledTimes(1) + }) + + it('has the correct target attribute', () => { + expect(wrapper.getDOMNode().getAttribute('target')).toBe( + mockProps.target + ) }) }) diff --git a/src/components/Link/index.tsx b/src/components/Link/index.tsx index 7cc2ccb3..72c37f01 100644 --- a/src/components/Link/index.tsx +++ b/src/components/Link/index.tsx @@ -1,6 +1,6 @@ import 'antd/lib/typography/style/index.css' import { createUseStyles } from 'react-jss' -import { linkColor } from '../../styles/styleguide' +import { linkColor } from 'styles/styleguide' import { Typography } from 'antd' import React, { FC, ReactNode } from 'react' @@ -8,25 +8,35 @@ const AntDLink = Typography.Link export type LinkTargetType = '_self' | '_blank' -export interface LinkProps { +interface SharedLinkProps { /** * Link children to render including link text. */ children: ReactNode /** - * The URL the link goes to. + * Where to open the linked url - either in a new tab or the current browsing context. */ - href?: string + target?: LinkTargetType +} + +interface LinkHref extends SharedLinkProps { /** - * Click handler. **Note**: While both `onClick` and `href` are optional, one of them is required. + * The URL the link goes to. */ - onClick?: () => void + href: string /** - * Where to open the linked url - either in a new tab or the current browsing context. + * Click handler. **Note**: Either an `onClick` or `href` is required. */ - target?: LinkTargetType + onClick?: never +} + +interface LinkClick extends SharedLinkProps { + href?: never + onClick: () => void } +export type LinkProps = LinkHref | LinkClick + interface AntDProps extends Omit { underline: boolean } @@ -54,9 +64,6 @@ const Link: FC = ({ underline: true } - if (!onClick && !href) - throw new Error('Link requires either an onClick or href prop.') - return {children} } diff --git a/src/components/Tag/Tag.stories.tsx b/src/components/Tag/Tag.stories.tsx index bc461cc7..78ac4d82 100644 --- a/src/components/Tag/Tag.stories.tsx +++ b/src/components/Tag/Tag.stories.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Meta, Story } from '@storybook/react/types-6-0' -import Tag, { TagProps } from './index' +import Tag, { TagProps } from '.' export default { argTypes: { diff --git a/src/components/Tag/Tag.test.tsx b/src/components/Tag/Tag.test.tsx index e060ba7a..c8a6883c 100644 --- a/src/components/Tag/Tag.test.tsx +++ b/src/components/Tag/Tag.test.tsx @@ -1,6 +1,6 @@ import React from 'react' import { shallow, ShallowWrapper } from 'enzyme' -import Tag, { TagProps } from './index' +import Tag, { TagProps } from '.' let wrapper: ShallowWrapper diff --git a/src/components/Toggle/Toggle.stories.tsx b/src/components/Toggle/Toggle.stories.tsx new file mode 100644 index 00000000..524d116e --- /dev/null +++ b/src/components/Toggle/Toggle.stories.tsx @@ -0,0 +1,30 @@ +import { action } from '@storybook/addon-actions' +import React from 'react' +import { Meta, Story } from '@storybook/react/types-6-0' +import Toggle, { ToggleProps } from '.' + +export default { + argTypes: { + onChange: { defaultValue: action('onChange') } + }, + component: Toggle, + title: 'Toggle' +} as Meta + +const Template: Story = args => + +export const Default = Template.bind({}) +Default.args = { + defaultChecked: true +} + +export const Disabled = Template.bind({}) +Disabled.args = { + disabled: true +} + +export const CheckedDisabled = Template.bind({}) +CheckedDisabled.args = { + defaultChecked: true, + disabled: true +} diff --git a/src/components/Toggle/Toggle.test.tsx b/src/components/Toggle/Toggle.test.tsx new file mode 100644 index 00000000..dcd1ee28 --- /dev/null +++ b/src/components/Toggle/Toggle.test.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { mount, ReactWrapper } from 'enzyme' +import Toggle, { ToggleProps } from '.' + +let wrapper: ReactWrapper +let mockChange: jest.Mock + +beforeEach(() => { + mockChange = jest.fn() + + const mockProps: ToggleProps = { + onChange: mockChange + } + + wrapper = mount() +}) + +describe('Toggle', () => { + it('renders', () => { + expect(wrapper).toHaveLength(1) + }) + + it('runs onChange function when toggle is clicked', () => { + expect(wrapper.simulate('click')) + expect(mockChange).toHaveBeenCalledTimes(1) + }) + + it('has the correct role attribute', () => { + expect(wrapper.getDOMNode().getAttribute('role')).toBe('switch') + }) + + it('has the correct aria-checked attribute', () => { + expect(wrapper.getDOMNode().getAttribute('aria-checked')).toBe('false') + }) + + it('changes the aria-checked attribute when it is clicked', () => { + wrapper.simulate('click') + expect(wrapper.getDOMNode().getAttribute('aria-checked')).toBe('true') + }) +}) diff --git a/src/components/Toggle/index.tsx b/src/components/Toggle/index.tsx new file mode 100644 index 00000000..035d0cc4 --- /dev/null +++ b/src/components/Toggle/index.tsx @@ -0,0 +1,40 @@ +import 'antd/lib/switch/style/index.css' +import { Switch } from 'antd' +import React, { FC } from 'react' + +export interface ToggleProps { + /** + * Required change handler. + */ + onChange: () => void + /** + * Whether switch will be checked or "on" by default. + */ + defaultChecked?: boolean + /** + * Whether switch will be disabled. + */ + disabled?: boolean + /** + * The size of the toggle. + */ + size?: 'default' | 'small' +} + +const Toggle: FC = ({ + onChange, + defaultChecked = false, + disabled = false, + size = 'default' +}: ToggleProps) => { + const antDProps = { + defaultChecked, + disabled, + onChange, + size + } + + return +} + +export default Toggle diff --git a/src/components/index.ts b/src/components/index.ts index 124c4d6e..e58de666 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,6 +1,8 @@ import 'normalize.css' -import '../styles/index.css' +import 'styles/index.css' export { default as Button } from './Button' export { default as InputField } from './InputField' +export { default as Icon } from './Icon' export { default as Link } from './Link' export { default as Tag } from './Tag' +export { default as Toggle } from './Toggle' diff --git a/tsconfig.rollup.json b/tsconfig.rollup.json index 4529cd84..64264ccc 100644 --- a/tsconfig.rollup.json +++ b/tsconfig.rollup.json @@ -1,5 +1,4 @@ { "extends": "./tsconfig.json", - "include": ["components/**/*"], "exclude": ["components/**/*.stories.tsx", "components/**/*.test.tsx"] }