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`] = `
+
+`;
+
+exports[`Storyshots Icon Predefined 1`] = `
+
+`;
+
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
+}
+
+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"]
}