diff --git a/components/Button/CopyLinkButton.js b/components/Button/CopyLinkButton.js index 63bdc5c..90e3230 100644 --- a/components/Button/CopyLinkButton.js +++ b/components/Button/CopyLinkButton.js @@ -1,16 +1,10 @@ // @flow // $FlowIssue import React, { useState } from 'react'; -import dynamic from 'next/dynamic'; -import * as Styled from './style'; +import { CopyLinkButton as StyledCopyLinkButton } from './style'; import Icon from '../Icon'; import type { ButtonProps } from './types'; -const Clipboard = dynamic(() => import('react-clipboard.js'), { - ssr: false, - loading: () => null, -}); - type CopyLinkProps = { ...$Exact, text: string, @@ -26,20 +20,18 @@ export default function CopyLinkButton(props: CopyLinkProps) { }; return ( - - - - {isClicked ? 'Copied!' : children} - - + + {isClicked ? 'Copied!' : children} + ); } diff --git a/components/Button/style.js b/components/Button/style.js index 37dbe8a..a5ad562 100644 --- a/components/Button/style.js +++ b/components/Button/style.js @@ -3,6 +3,12 @@ import styled, { css } from 'styled-components'; import { hexa, tint } from '../globals'; import type { ButtonSize } from './types'; import { theme } from '../theme'; +import dynamic from 'next/dynamic'; + +const Clipboard = dynamic(() => import('react-clipboard.js'), { + ssr: false, + loading: () => null, +}); const getPadding = (size: ButtonSize) => { switch (size) { @@ -32,7 +38,7 @@ const getFontSize = (size: ButtonSize) => { } }; -const base = css` +export const base = css` -webkit-appearance: none; display: flex; flex: none; @@ -58,11 +64,15 @@ const base = css` cursor: not-allowed; } - &:hover { + &:hover, &:active, &:focus { transition: all 0.2s ease-in-out; box-shadow: ${props => props.disabled ? 'none' : `${theme.shadows.button}`}; } + &:active, &:focus { + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.5)}; + } `; export const Button = styled.button` @@ -126,7 +136,7 @@ export const PrimaryButton = styled.button` &:focus { box-shadow: 0 0 0 1px ${props => props.theme.bg.default}, 0 0 0 3px ${props => - hexa(props.theme.brand.alt, 0.16)}; + hexa(props.theme.brand.alt, 0.5)}; } `; @@ -137,15 +147,15 @@ export const GhostButton = styled.button` background-color: transparent; background-image: none; - &:hover { - background: ${props => tint(props.theme.bg.wash, -3)}; + &:hover, &:active, &:focus { + background: ${props => tint(props.theme.bg.wash, -8)}; color: ${theme.text.default}; box-shadow: none; } - &:focus { + &:active, &:focus { box-shadow: 0 0 0 1px ${theme.bg.default}, - 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.08)}; + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; } `; @@ -220,12 +230,12 @@ export const ButtonSegmentRow = styled.div` ${PrimaryButton} { &:focus { box-shadow: 0 0 0 1px ${theme.bg.default}, - 0 0 0 3px ${props => hexa(props.theme.brand.alt, 0.16)}; + 0 0 0 3px ${props => hexa(props.theme.brand.alt, 0.5)}; } } `; -export const FacebookButton = styled.button` +export const FacebookButton = styled.a` ${base} border: 1px solid ${theme.social.facebook}; color: ${theme.bg.default}; @@ -262,11 +272,11 @@ export const FacebookButton = styled.button` &:focus { box-shadow: 0 0 0 1px ${props => props.theme.bg.default}, 0 0 0 3px ${props => - hexa(props.theme.social.facebook, 0.16)}; + hexa(props.theme.social.facebook, 0.5)}; } `; -export const TwitterButton = styled.button` +export const TwitterButton = styled.a` ${base} border: 1px solid ${theme.social.twitter}; color: ${theme.bg.default}; @@ -303,12 +313,13 @@ export const TwitterButton = styled.button` &:focus { box-shadow: 0 0 0 1px ${props => props.theme.bg.default}, 0 0 0 3px ${props => - hexa(props.theme.social.twitter, 0.16)}; + hexa(props.theme.social.twitter, 0.5)}; } `; -export const CopyLinkButton = styled.button` +export const CopyLinkButton = styled(Clipboard)` ${base} + transition: all ${theme.animations.default}; border: 1px solid ${props => props.isClicked ? tint(props.theme.success.default, -10) @@ -356,8 +367,9 @@ export const CopyLinkButton = styled.button` &:focus { box-shadow: 0 0 0 1px ${props => props.theme.bg.default}, 0 0 0 3px ${props => - props.isClicked - ? hexa(props.theme.success.default, 0.16) - : props.theme.border.default}; + props.isClicked + ? hexa(props.theme.success.default, 0.5) + : props.theme.border.default}; } `; + diff --git a/components/ChecklistItem/App.js b/components/ChecklistItem/App.js index 6b378d1..395f4a1 100644 --- a/components/ChecklistItem/App.js +++ b/components/ChecklistItem/App.js @@ -38,8 +38,8 @@ export const AppRow = ({ app }: Props) => { }; return ( - - + + {app.image && } {app.name} @@ -49,4 +49,4 @@ export const AppRow = ({ app }: Props) => { {app.offer && } ); -}; +}; \ No newline at end of file diff --git a/components/ChecklistItem/Apps.js b/components/ChecklistItem/Apps.js index e0a18e0..915b18f 100644 --- a/components/ChecklistItem/Apps.js +++ b/components/ChecklistItem/Apps.js @@ -1,23 +1,39 @@ // @flow // $FlowIssue -import React, { useState } from 'react'; -import { AppsContainer, SectionHeading, ExpandContainer } from './style'; +import React, { useState, useRef } from 'react'; +import { AppsContainer, SectionHeading, ExpandContainer, ExpandContent } from './style'; import { Button } from '../Button'; import type { ChecklistResource } from '../../types'; import { AppRow } from './App'; type Props = { resource: ChecklistResource, + handleAppsExpand: Function, }; -export const Apps = ({ resource }: Props) => { +export const Apps = ({ resource, handleAppsExpand }: Props) => { const [overflowExpanded, setOverflowExpanded] = useState(false); + const [contentHeight, setcontentHeight] = useState(2000); + + const expandContentContainer = useRef(null); + + function handleExpand() { + let expandContentHeight = + expandContentContainer.current + ? expandContentContainer.current.scrollHeight + : contentHeight; + + setcontentHeight(expandContentHeight) + setOverflowExpanded(!overflowExpanded) + + handleAppsExpand(overflowExpanded ? -expandContentHeight : expandContentHeight); + } if (!resource.apps) return null; let appList = resource.apps; let overflowAppList; - if (appList.length > 3) { + if (appList && appList.length > 3) { overflowAppList = appList.slice(3, appList.length); appList = appList.slice(0, 3); } @@ -25,19 +41,34 @@ export const Apps = ({ resource }: Props) => { return ( Apps - {appList.map(app => ( + {appList && appList.map(app => ( ))} - {overflowAppList && !overflowExpanded && ( - setOverflowExpanded(true)}> - - + {overflowAppList && ( + + + {overflowAppList.map(app => )} + + + + + + )} - - {overflowAppList && - overflowExpanded && - overflowAppList.map(app => )} ); }; + diff --git a/components/ChecklistItem/Heading.js b/components/ChecklistItem/Heading.js index 77c9fb5..9fa54e6 100644 --- a/components/ChecklistItem/Heading.js +++ b/components/ChecklistItem/Heading.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import { Title, Description, Uncollapse } from './style'; +import { Title, Uncollapse } from './style'; import type { ChecklistResource } from '../../types'; type Props = { @@ -13,10 +13,14 @@ export const Heading = ({ resource, isCollapsed, handleCollapse }: Props) => ( {resource.title} - <Uncollapse onClick={handleCollapse}> + <Uncollapse + onClick={handleCollapse} + aria-controls={`content_${resource.id}`} + aria-expanded={!isCollapsed} + type="button" + > {isCollapsed ? 'Show details' : 'Hide details'} </Uncollapse> - {!isCollapsed && } ); diff --git a/components/ChecklistItem/index.js b/components/ChecklistItem/index.js index c9fbeb1..2b9439b 100644 --- a/components/ChecklistItem/index.js +++ b/components/ChecklistItem/index.js @@ -10,10 +10,11 @@ import { Resources } from './Resources'; import { Container, CheckboxContainer, - Checkbox, CardContent, ResourceContent, Divider, + Description, + Content, } from './style'; type Props = { @@ -24,10 +25,18 @@ type State = { isChecked: boolean, isLoading: boolean, isCollapsed: boolean, + contentHeight: number, }; class ChecklistItem extends React.Component { - state = { isChecked: false, isLoading: true, isCollapsed: true }; + state = { isChecked: false, isLoading: true, isCollapsed: true, contentHeight: 2000, }; + contentContainer: { current: null | HTMLDivElement } + + constructor(props: Props) { + super(props); + + this.contentContainer = React.createRef(); + } componentDidMount() { const { resource } = this.props; @@ -39,6 +48,14 @@ class ChecklistItem extends React.Component { }); } + componentDidUpdate(prevProps: Props, prevState: State) { + if (prevState.isLoading && !this.state.isLoading && this.contentContainer.current) { + return this.setState({ + contentHeight: this.contentContainer.current.scrollHeight, + }) + } + } + handleSetChecked = () => { const { isChecked } = this.state; const { resource } = this.props; @@ -46,11 +63,19 @@ class ChecklistItem extends React.Component { return this.setState({ isChecked: !isChecked, isCollapsed: !isChecked }); }; - uncollapse = () => - this.setState(state => ({ isCollapsed: !state.isCollapsed })); + uncollapse = () => { + this.setState(state => ({ isCollapsed: !state.isCollapsed })); + this.contentContainer.current && this.contentContainer.current.focus(); + }; + + handleAppsExpand = (appsContainerHeight: number) => { + return this.contentContainer.current && this.setState({ + contentHeight: this.contentContainer.current.scrollHeight + appsContainerHeight, + }) + } render() { - const { isChecked, isLoading, isCollapsed } = this.state; + const { isChecked, isLoading, isCollapsed, contentHeight } = this.state; const { resource } = this.props; if (isLoading) return ; @@ -59,30 +84,60 @@ class ChecklistItem extends React.Component { - - + + + - + - {!isCollapsed && resource.apps && ( - - - - - )} + + + + + {resource.apps && ( + + + + + )} + + {resource.resources && ( + + + + + )} + + - {!isCollapsed && resource.resources && ( - - - - - )} diff --git a/components/ChecklistItem/style.js b/components/ChecklistItem/style.js index 4861b9d..f15df18 100644 --- a/components/ChecklistItem/style.js +++ b/components/ChecklistItem/style.js @@ -2,7 +2,7 @@ import styled, { css } from 'styled-components'; import Markdown from 'react-markdown'; import { theme } from '../theme'; -import { Shadows, tint } from '../globals'; +import { Shadows, tint, hexa } from '../globals'; export const Container = styled.div` margin-bottom: 24px; @@ -69,35 +69,29 @@ export const CheckboxContainer = styled.div` margin-top: 4px; width: 32px; } -`; -export const Checkbox = styled.span` - width: 32px; - height: 32px; - border-radius: 4px; - border: 1px solid - ${props => (props.isChecked ? theme.bg.default : theme.border.default)}; - background: ${props => - props.isChecked ? theme.success.default : theme.bg.wash}; - cursor: pointer; - position: relative; - background-image: ${props => - props.isChecked - ? 'radial-gradient(circle at top right, #a913de, #6ac9ff)' - : 'none'}; - box-shadow: ${props => - props.isChecked ? 'inset 0 0 1px rgba(0,0,0,0.4)' : 'none'}; - - &:hover { - ${props => !props.isChecked && Shadows.default}; - background: ${theme.bg.default}; - background-image: ${props => - props.isChecked - ? 'radial-gradient(circle at top right, #a913de, #6ac9ff)' - : 'none'}; + input[type="checkbox"] { + position: absolute; } - &:after { + input[type="checkbox"] + label { + width: 32px; + height: 32px; + border-radius: 4px; + border: 1px solid ${theme.border.default}; + background: ${theme.bg.wash}; + cursor: pointer; + position: relative; + overflow: hidden; + text-indent: -1000px; + } + + input[type="checkbox"] + label:hover { + ${Shadows.default}; + background: ${theme.bg.default}; + } + + input[type="checkbox"] + label::after { content: ''; position: absolute; display: block; @@ -105,11 +99,49 @@ export const Checkbox = styled.span` top: 6px; width: 6px; height: 12px; - border: solid - ${props => (props.isChecked ? theme.bg.default : theme.border.active)}; + border: solid ${theme.border.active}; border-width: 0 2px 2px 0; transform: rotate(45deg); } + + input[type="checkbox"]:checked + label { + border: 1px solid ${theme.bg.default}; + } + + input[type="checkbox"]:checked + label::after { + border: solid ${theme.bg.default}; + border-width: 0 2px 2px 0; + } + + /* This ::before pseudo-element is used to animate the gradient + which does not support transitions. */ + input[type="checkbox"] + label::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + transition: opacity ${theme.animations.default}; + opacity: 0; + background-image: radial-gradient(circle at top right, #a913de, #6ac9ff); + box-shadow: inset 0 0 1px rgba(0,0,0,0.4); + } + + input[type="checkbox"]:checked + label::before { + opacity: 1; + background-color: ${theme.success.default}; + } + + input[type="checkbox"]:active + label, input[type="checkbox"]:focus + label { + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.brand.default, 0.5)}; + } + input[type="checkbox"]:active:checked + label, input[type="checkbox"]:focus:checked + label { + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.spectrum.default, 0.5)}; + } + `; export const ResourceContent = styled.div` @@ -129,12 +161,7 @@ export const ResourceContent = styled.div` export const AppsContainer = styled.div``; -export const AppMeta = styled.div` - display: flex; - align-items: center; -`; - -export const AppRowContainer = styled.a` +export const AppRowContainer = styled.div` display: flex; justify-content: space-between; align-items: center; @@ -170,6 +197,19 @@ export const AppRowContainer = styled.a` } `; +export const AppMeta = styled.a` + display: flex; + align-items: center; + padding-right: 6px; + border-radius: 8px; + + &:active, &:focus { + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; + background: ${theme.bg.wash}; + } +`; + export const AppIcon = styled.img` width: 40px; height: 40px; @@ -207,8 +247,7 @@ export const AppSourcesListItem = styled.li` align-items: center; justify-content: center; color: ${theme.text.tertiary}; - padding: 4px 10px; - border-radius: 4px; + padding: 2px 8px; min-width: 56px; transition: all 0.1s ease-in-out; @@ -217,6 +256,19 @@ export const AppSourcesListItem = styled.li` flex-direction: column; align-items: center; justify-content: center; + padding: 2px; + border-radius: 8px; + + &:hover { + color: ${theme.text.default}; + } + + &:active, &:focus { + color: ${theme.text.default}; + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; + background: ${theme.bg.wash}; + } } .icon { @@ -225,9 +277,6 @@ export const AppSourcesListItem = styled.li` left: 4px; } - &:hover { - color: ${theme.text.default}; - } @media (max-width: 768px) { padding: 4px; @@ -280,11 +329,16 @@ export const ResourceRowContainer = styled.a` margin-right: 8px; } - &:hover { + &:hover, &:active, &:focus { background: ${theme.bg.wash}; color: ${theme.text.default}; } + &:active, &:focus { + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; + } + @media (max-width: 768px) { width: calc(100% + 40px); border-radius: 0; @@ -292,7 +346,7 @@ export const ResourceRowContainer = styled.a` margin-left: -24px; padding-left: 24px; - &:hover { + &:hover, &:active, &:focus { background: ${theme.bg.default}!important; } @@ -309,7 +363,7 @@ export const ResourceName = styled.p` font-weight: 400; `; -export const Divider = styled.div` +export const Divider = styled.hr` border-bottom: 1px solid ${tint(theme.bg.wash, -4)}; margin-top: 24px; margin-bottom: 24px; @@ -321,7 +375,27 @@ export const Divider = styled.div` } `; -export const Uncollapse = styled.span` +export const Content = styled.div` + transition: + max-height ${theme.animations.default}, + opacity ${theme.animations.default}, + visibility ${theme.animations.default}; + + max-height: 2000px; + max-height: var(--maxHeight); + opacity: 1; + visibility: visible; + + + &[aria-hidden="true"] { + max-height: 0; + opacity: 0; + visibility: hidden; + } + +` + +export const Uncollapse = styled.button` background: ${theme.bg.wash}; border-radius: 20px; padding: 8px 16px; @@ -344,6 +418,12 @@ export const Uncollapse = styled.span` color: ${theme.text.default}; background: ${tint(theme.bg.wash, -4)}; } + &:active, &:focus { + transition: all 0.2s ease-in-out; + + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; + } `; export const OfferContainer = styled.a` @@ -379,6 +459,12 @@ export const OfferContainer = styled.a` &:hover { color: ${theme.text.secondary}; } + &:active, &:focus { + box-shadow: inset 0 0 1px ${theme.border.active}, + 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; + background: ${theme.bg.wash}; + } `; export const LeftBorder = styled.div` @@ -417,3 +503,22 @@ export const ExpandContainer = styled.div` width: calc(100% + 40px); } `; + + +export const ExpandContent = styled.div` + transition: + max-height ${theme.animations.default}, + opacity ${theme.animations.default}, + visibility ${theme.animations.default}; + max-height: 2000px; + max-height: var(--maxHeight); + opacity: 1; + visibility: visible; + + &[aria-hidden="true"] { + max-height: 0; + opacity: 0; + visibility: hidden; + } + +` \ No newline at end of file diff --git a/components/Footer/style.js b/components/Footer/style.js index ea3a1e5..b727f88 100644 --- a/components/Footer/style.js +++ b/components/Footer/style.js @@ -1,6 +1,7 @@ // @flow import styled from 'styled-components'; import { theme } from '../theme'; +import { hexa } from '../globals'; export const Container = styled.div` margin-top: 128px; @@ -21,24 +22,32 @@ export const Description = styled.p` color: ${theme.text.default}; margin-left: 4px; } + a:active, a:focus { + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; + } `; export const Icons = styled.div` display: flex; flex: 1 0 auto; align-items: flex-start; - margin-left: -16px; padding-bottom: 8px; a { color: ${theme.text.tertiary}; + margin-right: 16px; + line-height: 1; + border-radius: 10px; + height: 32px; } - a:hover { + a:hover, a:active, a:focus { color: ${theme.text.default}; } - .icon { - margin-left: 16px; + a:active, a:focus { + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; } `; diff --git a/components/Header/index.js b/components/Header/index.js index 03b31df..3552c43 100644 --- a/components/Header/index.js +++ b/components/Header/index.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; import Link from 'next/link'; -import { Container, ButtonRowContainer, Label } from './style'; +import { Container, ButtonRowContainer, Label, LogoLink } from './style'; import { PrimaryButton, GhostButton } from '../Button'; import Logo from './Logo'; @@ -14,27 +14,30 @@ export default function Header(props: Props) { return ( - - - - - - +
+ + + + + + +
- - About - + + About + - - Contribute - + Contribute +
); diff --git a/components/Header/style.js b/components/Header/style.js index 4ad6b1e..8492fb2 100644 --- a/components/Header/style.js +++ b/components/Header/style.js @@ -1,6 +1,7 @@ // @flow import styled from 'styled-components'; import { theme } from '../theme'; +import { hexa } from '../globals'; export const Container = styled.div` display: grid; @@ -18,10 +19,6 @@ export const Container = styled.div` props.showHeaderShadow ? '0 4px 8px rgba(0,0,0,0.04)' : 'none'}; transition: all 0.2s ease-in-out; - a { - margin-left: 8px; - } - @media (max-width: 968px) { padding: 8px 16px; grid-template-columns: 1fr 1fr; @@ -41,6 +38,26 @@ export const ButtonRowContainer = styled.div` justify-content: flex-end; grid-area: actions; align-items: center; + + a { + margin-left: 8px; + } +`; + +export const LogoLink = styled.a` + transition: all ${props => props.theme.animations.default}; + display: inline-flex; + align-items: center; + border-radius: 6px; + + &:hover { + transform: scale(1.2); + } + &:active, &:focus { + transform: scale(1.2); + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; + } `; export const Label = styled.h1` diff --git a/components/Page/index.js b/components/Page/index.js index c3075cc..13fddb7 100644 --- a/components/Page/index.js +++ b/components/Page/index.js @@ -74,7 +74,11 @@ export default function Page(props: Props) {
{children}