mirror of
https://github.com/brianlovin/security-checklist.git
synced 2026-03-11 08:55:31 +00:00
Merge pull request #128 from joachimesque/accessible-checkboxes
[A11y] Better accessibility: checkboxes, buttons & links, visible/invisible content toggles
This commit is contained in:
commit
406d33dd5e
16 changed files with 400 additions and 157 deletions
|
|
@ -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<ButtonProps>,
|
||||
text: string,
|
||||
|
|
@ -26,20 +20,18 @@ export default function CopyLinkButton(props: CopyLinkProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Clipboard
|
||||
style={{ background: 'none' }}
|
||||
<StyledCopyLinkButton
|
||||
data-clipboard-text={text}
|
||||
onSuccess={onClick}
|
||||
component="a"
|
||||
component="button"
|
||||
data-cy="copy-link-button"
|
||||
isClicked={isClicked}
|
||||
aria-label="Copy the website’s address to your clipboard."
|
||||
type="button"
|
||||
{...props}
|
||||
>
|
||||
<Styled.CopyLinkButton
|
||||
data-cy="copy-link-button"
|
||||
isClicked={isClicked}
|
||||
{...props}
|
||||
>
|
||||
<Icon glyph="link" size={24} />
|
||||
{isClicked ? 'Copied!' : children}
|
||||
</Styled.CopyLinkButton>
|
||||
</Clipboard>
|
||||
<Icon glyph="link" size={24} />
|
||||
<span>{isClicked ? 'Copied!' : children}</span>
|
||||
</StyledCopyLinkButton>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ export const AppRow = ({ app }: Props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<AppRowContainer href={app.url} target="_blank" rel="noopener noreferrer">
|
||||
<AppMeta>
|
||||
<AppRowContainer>
|
||||
<AppMeta href={app.url} target="_blank" rel="noopener noreferrer" title={`go to: ${app.name}`}>
|
||||
{app.image && <AppIcon alt={app.name} src={app.image} />}
|
||||
<AppName>{app.name}</AppName>
|
||||
</AppMeta>
|
||||
|
|
@ -49,4 +49,4 @@ export const AppRow = ({ app }: Props) => {
|
|||
{app.offer && <Offer offer={app.offer} />}
|
||||
</AppRowContainer>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<AppsContainer overflowExpanded={overflowExpanded}>
|
||||
<SectionHeading>Apps</SectionHeading>
|
||||
{appList.map(app => (
|
||||
{appList && appList.map(app => (
|
||||
<AppRow key={app.name} app={app} />
|
||||
))}
|
||||
|
||||
{overflowAppList && !overflowExpanded && (
|
||||
<ExpandContainer onClick={() => setOverflowExpanded(true)}>
|
||||
<Button>Show more choices</Button>
|
||||
</ExpandContainer>
|
||||
{overflowAppList && (
|
||||
<React.Fragment>
|
||||
<ExpandContent
|
||||
id={`apps_${resource.id}`}
|
||||
role="region"
|
||||
tabindex="-1"
|
||||
ref={expandContentContainer}
|
||||
style={{ '--maxHeight': `${contentHeight}px` }}
|
||||
aria-hidden={!overflowExpanded}
|
||||
>
|
||||
{overflowAppList.map(app => <AppRow key={app.name} app={app} />)}
|
||||
</ExpandContent>
|
||||
|
||||
<ExpandContainer
|
||||
onClick={handleExpand}
|
||||
role="button"
|
||||
aria-expanded={overflowExpanded}
|
||||
aria-controls={`apps_${resource.id}`}
|
||||
>
|
||||
<Button type="button">Show {overflowExpanded ? "less" : "more"} choices</Button>
|
||||
</ExpandContainer>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{overflowAppList &&
|
||||
overflowExpanded &&
|
||||
overflowAppList.map(app => <AppRow key={app.name} app={app} />)}
|
||||
</AppsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => (
|
|||
<React.Fragment>
|
||||
<Title>
|
||||
{resource.title}
|
||||
<Uncollapse onClick={handleCollapse}>
|
||||
<Uncollapse
|
||||
onClick={handleCollapse}
|
||||
aria-controls={`content_${resource.id}`}
|
||||
aria-expanded={!isCollapsed}
|
||||
type="button"
|
||||
>
|
||||
{isCollapsed ? 'Show details' : 'Hide details'}
|
||||
</Uncollapse>
|
||||
</Title>
|
||||
{!isCollapsed && <Description source={resource.description} />}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<Props, State> {
|
||||
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<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
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<Props, State> {
|
|||
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 <LoadingChecklistItem />;
|
||||
|
|
@ -59,30 +84,60 @@ class ChecklistItem extends React.Component<Props, State> {
|
|||
<Container>
|
||||
<Card isChecked={isChecked}>
|
||||
<CardContent isCollapsed={isCollapsed}>
|
||||
<CheckboxContainer onClick={this.handleSetChecked}>
|
||||
<Checkbox isChecked={isChecked} />
|
||||
<CheckboxContainer>
|
||||
<input
|
||||
aria-expanded={!isCollapsed}
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
id={`checkbox_${resource.id}`}
|
||||
onChange={this.handleSetChecked}
|
||||
aria-controls={`content_${resource.id}`}
|
||||
/>
|
||||
<label htmlFor={`checkbox_${resource.id}`}>
|
||||
{resource.title}
|
||||
</label>
|
||||
</CheckboxContainer>
|
||||
|
||||
<ResourceContent isChecked={isChecked} isCollapsed={isCollapsed}>
|
||||
<ResourceContent
|
||||
isChecked={isChecked}
|
||||
isCollapsed={isCollapsed}
|
||||
>
|
||||
<Heading
|
||||
resource={resource}
|
||||
isCollapsed={isCollapsed}
|
||||
handleCollapse={this.uncollapse}
|
||||
/>
|
||||
|
||||
{!isCollapsed && resource.apps && (
|
||||
<React.Fragment>
|
||||
<Divider />
|
||||
<Apps resource={resource} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Content
|
||||
aria-hidden={isCollapsed}
|
||||
id={`content_${resource.id}`}
|
||||
role="region"
|
||||
tabindex="-1"
|
||||
ref={this.contentContainer}
|
||||
style={{ '--maxHeight': `${contentHeight}px` }}
|
||||
>
|
||||
|
||||
<Description source={resource.description} />
|
||||
|
||||
{resource.apps && (
|
||||
<React.Fragment>
|
||||
<Divider />
|
||||
<Apps
|
||||
resource={resource}
|
||||
handleAppsExpand={this.handleAppsExpand}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{resource.resources && (
|
||||
<React.Fragment>
|
||||
<Divider />
|
||||
<Resources resource={resource} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
</Content>
|
||||
|
||||
{!isCollapsed && resource.resources && (
|
||||
<React.Fragment>
|
||||
<Divider />
|
||||
<Resources resource={resource} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ResourceContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
`
|
||||
|
|
@ -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)};
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Container showHeaderShadow={showHeaderShadow} data-cy="header">
|
||||
<Link href="/">
|
||||
<a style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Label>Security Checklist</Label>
|
||||
<Logo />
|
||||
</a>
|
||||
</Link>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<LogoLink href="/">
|
||||
<Label>Security Checklist</Label>
|
||||
<Logo />
|
||||
</LogoLink>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ButtonRowContainer>
|
||||
<Link href="/about">
|
||||
<a>
|
||||
<GhostButton>About</GhostButton>
|
||||
</a>
|
||||
<GhostButton as="a" href="/about">
|
||||
About
|
||||
</GhostButton>
|
||||
</Link>
|
||||
|
||||
<a
|
||||
<PrimaryButton
|
||||
href="https://github.com/brianlovin/security-checklist"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
as="a"
|
||||
>
|
||||
<PrimaryButton>Contribute</PrimaryButton>
|
||||
</a>
|
||||
Contribute
|
||||
</PrimaryButton>
|
||||
</ButtonRowContainer>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@ export default function Page(props: Props) {
|
|||
<Header showHeaderShadow={showHeaderShadow} />
|
||||
<InnerContainer>{children}</InnerContainer>
|
||||
<Footer />
|
||||
<ScrollToTop isVisible={scrollToTopVisible} onClick={scrollToTop}>
|
||||
<ScrollToTop
|
||||
isVisible={scrollToTopVisible}
|
||||
onClick={scrollToTop}
|
||||
type="button"
|
||||
>
|
||||
<Icon glyph="view-forward" size={32} />
|
||||
</ScrollToTop>
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import { tint } from '../globals';
|
||||
import { hexa, tint } from '../globals';
|
||||
import { theme } from '../theme';
|
||||
|
||||
export const Container = styled.div`
|
||||
|
|
@ -47,7 +47,7 @@ export const SectionHeading = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
export const Heading = styled.h3`
|
||||
export const Heading = styled.h2`
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
color: ${theme.text.default};
|
||||
|
|
@ -58,7 +58,7 @@ export const Heading = styled.h3`
|
|||
}
|
||||
`;
|
||||
|
||||
export const Subheading = styled.h4`
|
||||
export const Subheading = styled.p`
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
color: ${theme.text.tertiary};
|
||||
|
|
@ -69,6 +69,18 @@ export const Subheading = styled.h4`
|
|||
color: ${theme.text.default};
|
||||
font-weight: 500;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: ${theme.text.tertiary};
|
||||
}
|
||||
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)};
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 968px) {
|
||||
max-width: 100%;
|
||||
|
|
@ -113,8 +125,11 @@ export const ScrollToTop = styled.button`
|
|||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.09);
|
||||
&:active, &:focus {
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.09),
|
||||
0 0 0 1px ${theme.bg.default},
|
||||
0 0 0 3px ${props => hexa(props.theme.brand.default, 0.5)};
|
||||
outline: none;
|
||||
transform: translateY(-2px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,21 +6,21 @@ import { Container } from './style';
|
|||
export default function ShareButtons() {
|
||||
return (
|
||||
<Container>
|
||||
<a
|
||||
<FacebookButton
|
||||
href="https://www.facebook.com/sharer/sharer.php?u=https://securitycheckli.st"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FacebookButton>Share</FacebookButton>
|
||||
</a>
|
||||
Share
|
||||
</FacebookButton>
|
||||
|
||||
<a
|
||||
<TwitterButton
|
||||
href="http://twitter.com/share?text=Check out Security Checklist, a checklist to help people stay safe online&url=https://securitycheckli.st"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<TwitterButton>Tweet</TwitterButton>
|
||||
</a>
|
||||
Tweet
|
||||
</TwitterButton>
|
||||
|
||||
<CopyLinkButton text="https://securitycheckli.st">
|
||||
Share Link
|
||||
|
|
|
|||
|
|
@ -13,10 +13,8 @@ export const Container = styled.div`
|
|||
|
||||
@media (max-width: 568px) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 0;
|
||||
|
||||
button {
|
||||
margin-top: 16px;
|
||||
a, button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export const getItemFromStorage = key => {
|
|||
if (!localStorage) return;
|
||||
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(key));
|
||||
return JSON.parse(localStorage.getItem(key) || false);
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -51,8 +51,6 @@ export default function About() {
|
|||
.
|
||||
</Subheading>
|
||||
|
||||
<div style={{ padding: '8px' }} />
|
||||
|
||||
<Subheading>
|
||||
The code that powers this website is{' '}
|
||||
<a
|
||||
|
|
|
|||
Loading…
Reference in a new issue