Merge pull request #19 from GRA0007/dev

Performance, a11y and caching
This commit is contained in:
Benjamin Grant 2021-06-01 04:54:25 +10:00 committed by GitHub
commit 968a7423e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 300 additions and 132 deletions

View file

@ -0,0 +1,11 @@
module.exports = function override(config, env) {
config.output.filename = env === 'production'
? 'static/js/[name].[contenthash].js'
: env === 'development' && 'static/js/bundle.js';
config.output.chunkFilename = env === 'production'
? 'static/js/[name].[contenthash].chunk.js'
: env === 'development' && 'static/js/[name].chunk.js';
return config;
}

View file

@ -41,9 +41,9 @@
"zustand": "^3.3.2" "zustand": "^3.3.2"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-app-rewired start",
"build": "react-scripts build", "build": "react-app-rewired build",
"test": "react-scripts test", "test": "react-app-rewired test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
@ -63,5 +63,8 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"react-app-rewired": "^2.1.8"
} }
} }

View file

@ -36,7 +36,7 @@
</script> </script>
</head> </head>
<body> <body>
<noscript style="font-family: Karla, sans-serif; text-align: center; margin: 20vh 0; display: block;"><h1>🦀 Crab Fit doesn't work without Javascript 🏋️</h1><p>Enable Javascript or try a different browser.<p></noscript> <noscript style="font-family: Karla, sans-serif; text-align: center; margin: 20vh 0; display: block;"><h1>🦀 Crab Fit doesn't work without Javascript 🏋️</h1><p>Enable Javascript or try a different browser.</p></noscript>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View file

@ -107,6 +107,11 @@ const App = () => {
}, },
})} })}
/> />
<Suspense fallback={<Loading />}>
<Settings />
</Suspense>
<Switch> <Switch>
<Route path="/" exact render={props => ( <Route path="/" exact render={props => (
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
@ -135,10 +140,6 @@ const App = () => {
)} /> )} />
</Switch> </Switch>
<Suspense fallback={<Loading />}>
<Settings />
</Suspense>
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />} {eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
</ThemeProvider> </ThemeProvider>
</BrowserRouter> </BrowserRouter>

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useRef, Fragment } from 'react'; import { useState, useEffect, useRef, useMemo, Fragment } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData'; import localeData from 'dayjs/plugin/localeData';
@ -65,6 +65,94 @@ const AvailabilityViewer = ({
setTouched(people.length <= 1); setTouched(people.length <= 1);
}, [people]); }, [people]);
const heatmap = useMemo(() => (
<Container>
<TimeLabels>
{!!timeLabels.length && timeLabels.map((label, i) =>
<TimeSpace key={i}>
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
</TimeSpace>
)}
</TimeLabels>
{dates.map((date, i) => {
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1;
return (
<Fragment key={i}>
<Date>
{isSpecificDates && <DateLabel locale={locale}>{parsedDate.format('MMM D')}</DateLabel>}
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
<Times
borderRight={last}
borderLeft={i === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[i-1], 'DDMMYYYY') : dayjs().day(dates[i-1]), 'day') > 1}
>
{timeLabels.map((timeLabel, i) => {
if (!timeLabel.time) return null;
if (!times.includes(`${timeLabel.time}-${date}`)) {
return (
<TimeSpace key={i} />
);
}
const time = `${timeLabel.time}-${date}`;
const peopleHere = tempFocus !== null
? people.filter(person => person.availability.includes(time) && tempFocus === person.name).map(person => person.name)
: people.filter(person => person.availability.includes(time) && filteredPeople.includes(person.name)).map(person => person.name);
return (
<Time
key={i}
time={time}
className="time"
peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
aria-label={peopleHere.join(', ')}
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
highlight={highlight}
onMouseEnter={(e) => {
const cellBox = e.currentTarget.getBoundingClientRect();
const wrapperBox = wrapper?.current?.getBoundingClientRect() ?? { x: 0, y: 0 };
const timeText = timeFormat === '12h' ? `h${locales[locale].separator}mma` : `HH${locales[locale].separator}mm`;
setTooltip({
x: Math.round(cellBox.x-wrapperBox.x + cellBox.width/2),
y: Math.round(cellBox.y-wrapperBox.y + cellBox.height)+6,
available: `${peopleHere.length} / ${people.length} ${t('event:available')}`,
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
people: peopleHere,
});
}}
onMouseLeave={() => {
setTooltip(null);
}}
/>
);
})}
</Times>
</Date>
{last && dates.length !== i+1 && (
<Spacer />
)}
</Fragment>
);
})}
</Container>
), [
people,
filteredPeople,
tempFocus,
focusCount,
highlight,
locale,
dates,
isSpecificDates,
max,
min,
t,
timeFormat,
timeLabels,
times,
]);
return ( return (
<> <>
<StyledMain> <StyledMain>
@ -108,76 +196,8 @@ const AvailabilityViewer = ({
<Wrapper ref={wrapper}> <Wrapper ref={wrapper}>
<ScrollWrapper> <ScrollWrapper>
<Container> {heatmap}
<TimeLabels>
{!!timeLabels.length && timeLabels.map((label, i) =>
<TimeSpace key={i}>
{label.label?.length !== '' && <TimeLabel>{label.label}</TimeLabel>}
</TimeSpace>
)}
</TimeLabels>
{dates.map((date, i) => {
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1;
return (
<Fragment key={i}>
<Date>
{isSpecificDates && <DateLabel locale={locale}>{parsedDate.format('MMM D')}</DateLabel>}
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
<Times
borderRight={last}
borderLeft={i === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[i-1], 'DDMMYYYY') : dayjs().day(dates[i-1]), 'day') > 1}
>
{timeLabels.map((timeLabel, i) => {
if (!timeLabel.time) return null;
if (!times.includes(`${timeLabel.time}-${date}`)) {
return (
<TimeSpace key={i} />
);
}
const time = `${timeLabel.time}-${date}`;
const peopleHere = tempFocus !== null
? people.filter(person => person.availability.includes(time) && tempFocus === person.name).map(person => person.name)
: people.filter(person => person.availability.includes(time) && filteredPeople.includes(person.name)).map(person => person.name);
return (
<Time
key={i}
time={time}
className="time"
peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
aria-label={peopleHere.join(', ')}
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
highlight={highlight}
onMouseEnter={(e) => {
const cellBox = e.currentTarget.getBoundingClientRect();
const wrapperBox = wrapper?.current?.getBoundingClientRect() ?? { x: 0, y: 0 };
const timeText = timeFormat === '12h' ? `h${locales[locale].separator}mma` : `HH${locales[locale].separator}mm`;
setTooltip({
x: Math.round(cellBox.x-wrapperBox.x + cellBox.width/2),
y: Math.round(cellBox.y-wrapperBox.y + cellBox.height)+6,
available: `${peopleHere.length} / ${people.length} ${t('event:available')}`,
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
people: peopleHere,
});
}}
onMouseLeave={() => {
setTooltip(null);
}}
/>
);
})}
</Times>
</Date>
{last && dates.length !== i+1 && (
<Spacer />
)}
</Fragment>
);
})}
</Container>
{tooltip && ( {tooltip && (
<Tooltip <Tooltip
x={tooltip.x} x={tooltip.x}

View file

@ -80,7 +80,7 @@ export const Time = styled.div`
border-top: 2px dotted ${props.theme.text}; border-top: 2px dotted ${props.theme.text};
`} `}
${props => props.highlight && props.peopleCount === props.maxPeople ? ` ${props => props.highlight && props.peopleCount === props.maxPeople && props.peopleCount > 0 ? `
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
45deg, 45deg,
${props.theme.primary}, ${props.theme.primary},

View file

@ -168,7 +168,17 @@ const CalendarField = ({
selected={selectedDates.includes(date.format('DDMMYYYY'))} selected={selectedDates.includes(date.format('DDMMYYYY'))}
selecting={selectingDates.includes(date)} selecting={selectingDates.includes(date)}
mode={mode} mode={mode}
onPointerDown={(e) => { type="button"
onKeyPress={e => {
if (e.key === ' ' || e.key === 'Enter') {
if (selectedDates.includes(date.format('DDMMYYYY'))) {
setSelectedDates(selectedDates.filter(d => d !== date.format('DDMMYYYY')));
} else {
setSelectedDates([...selectedDates, date.format('DDMMYYYY')]);
}
}
}}
onPointerDown={e => {
startPos.current = {x, y}; startPos.current = {x, y};
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add'); setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
setSelectingDates([date]); setSelectingDates([date]);

View file

@ -51,21 +51,27 @@ export const CalendarBody = styled.div`
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
grid-gap: 2px; grid-gap: 2px;
& div:first-of-type { & button:first-of-type {
border-top-left-radius: 3px; border-top-left-radius: 3px;
} }
& div:nth-of-type(7) { & button:nth-of-type(7) {
border-top-right-radius: 3px; border-top-right-radius: 3px;
} }
& div:nth-last-of-type(7) { & button:nth-last-of-type(7) {
border-bottom-left-radius: 3px; border-bottom-left-radius: 3px;
} }
& div:last-of-type { & button:last-of-type {
border-bottom-right-radius: 3px; border-bottom-right-radius: 3px;
} }
`; `;
export const Date = styled.div` export const Date = styled.button`
font: inherit;
color: inherit;
background: none;
border: 0;
appearance: none;
background-color: ${props => props.theme.primaryBackground}; background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primaryLight}; border: 1px solid ${props => props.theme.primaryLight};
display: flex; display: flex;

View file

@ -1,15 +1,33 @@
import { useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Button } from 'components'; import { Button } from 'components';
import { useTWAStore } from 'stores'; import { useTWAStore } from 'stores';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import {
Wrapper,
Options,
} from './donateStyle';
const PAYMENT_METHOD = 'https://play.google.com/billing'; const PAYMENT_METHOD = 'https://play.google.com/billing';
const SKU = 'crab_donation'; const SKU = 'crab_donation';
const Donate = ({ onDonate = null }) => { const Donate = () => {
const store = useTWAStore(); const store = useTWAStore();
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const firstLinkRef = useRef();
const buttonRef = useRef();
const modalRef = useRef();
const [isOpen, _setIsOpen] = useState(false);
const setIsOpen = open => {
_setIsOpen(open);
if (open) {
window.setTimeout(() => firstLinkRef.current.focus(), 150);
}
};
useEffect(() => { useEffect(() => {
if (store.TWA === undefined) { if (store.TWA === undefined) {
store.setTWA(document.referrer.includes('android-app://fit.crab')); store.setTWA(document.referrer.includes('android-app://fit.crab'));
@ -71,7 +89,7 @@ const Donate = ({ onDonate = null }) => {
}; };
return ( return (
<div style={{ marginTop: 6, marginLeft: 12 }}> <Wrapper>
<a <a
onClick={event => { onClick={event => {
gtag('event', 'donate', { 'event_category': 'donate' }); gtag('event', 'donate', { 'event_category': 'donate' });
@ -82,14 +100,15 @@ const Donate = ({ onDonate = null }) => {
alert(t('donate.messages.error')); alert(t('donate.messages.error'));
} }
} }
} else if (onDonate !== null) { } else {
event.preventDefault(); event.preventDefault();
onDonate(); setIsOpen(true);
} }
}} }}
href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=5" href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=5"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
ref={buttonRef}
> >
<Button <Button
buttonHeight="30px" buttonHeight="30px"
@ -99,7 +118,21 @@ const Donate = ({ onDonate = null }) => {
title={t('donate.title')} title={t('donate.title')}
>{t('donate.button')}</Button> >{t('donate.button')}</Button>
</a> </a>
</div>
<Options
isOpen={isOpen}
ref={modalRef}
onBlur={e => {
if (modalRef.current.contains(e.relatedTarget)) return;
setIsOpen(false);
}}
>
<a onClick={() => setIsOpen(false)} ref={firstLinkRef} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=2" target="_blank" rel="noreferrer">{t('donate.options.$2')}</a>
<a onClick={() => setIsOpen(false)} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=5" target="_blank" rel="noreferrer"><strong>{t('donate.options.$5')}</strong></a>
<a onClick={() => setIsOpen(false)} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=10" target="_blank" rel="noreferrer">{t('donate.options.$10')}</a>
<a onClick={() => setIsOpen(false)} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD" target="_blank" rel="noreferrer">{t('donate.options.choose')}</a>
</Options>
</Wrapper>
); );
} }

View file

@ -0,0 +1,52 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
margin-top: 6px;
margin-left: 12px;
position: relative;
`;
export const Options = styled.div`
position: absolute;
bottom: calc(100% + 20px);
right: 0;
background-color: ${props => props.theme.background};
${props => props.theme.mode === 'dark' && `
border: 1px solid ${props.theme.primaryBackground};
`}
z-index: 60;
padding: 4px 10px;
border-radius: 14px;
box-sizing: border-box;
max-width: calc(100vw - 20px);
box-shadow: 0 3px 6px 0 rgba(0,0,0,.3);
visibility: hidden;
pointer-events: none;
opacity: 0;
transform: translateY(5px);
transition: opacity .15s, transform .15s, visibility .15s;
${props => props.isOpen && `
pointer-events: all;
opacity: 1;
transform: translateY(0);
visibility: visible;
`}
& a {
display: block;
white-space: nowrap;
text-align: center;
padding: 4px 20px;
margin: 6px 0;
text-decoration: none;
border-radius: 100px;
background-color: ${props => props.theme.primary};
color: ${props => props.theme.background};
&:hover {
text-decoration: underline;
}
}
`;

View file

@ -1,28 +1,15 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Donate } from 'components'; import { Donate } from 'components';
import { Wrapper, Link } from './footerStyle'; import { Wrapper } from './footerStyle';
const Footer = (props) => { const Footer = (props) => {
const [donateMode, setDonateMode] = useState(false);
const { t } = useTranslation('common'); const { t } = useTranslation('common');
return ( return (
<Wrapper id="donate" donateMode={donateMode} {...props}> <Wrapper id="donate" {...props}>
{donateMode ? ( <span>{t('donate.info')}</span>
<> <Donate />
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=2" target="_blank">{t('donate.options.$2')}</Link>
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=5" target="_blank"><strong>{t('donate.options.$5')}</strong></Link>
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=10" target="_blank">{t('donate.options.$10')}</Link>
<Link href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD" target="_blank">{t('donate.options.choose')}</Link>
</>
) : (
<>
<span>{t('donate.info')}</span>
<Donate onDonate={() => setDonateMode(true)} />
</>
)}
</Wrapper> </Wrapper>
); );
}; };

View file

@ -19,20 +19,4 @@ export const Wrapper = styled.footer`
margin-bottom: 20px; margin-bottom: 20px;
} }
`} `}
${props => props.donateMode && `
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
`}
`;
export const Link = styled.a`
padding: 11px 10px;
white-space: nowrap;
& strong {
font-weight: 800;
}
`; `;

View file

@ -34,7 +34,7 @@ const Legend = ({
<Grade <Grade
key={i} key={i}
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`} color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
highlight={highlight && i === max} highlight={highlight && i === max && max > 0}
onMouseOver={() => onSegmentFocus(i)} onMouseOver={() => onSegmentFocus(i)}
/> />
)} )}

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -27,9 +27,27 @@ const setDefaults = (lang, store) => {
const Settings = () => { const Settings = () => {
const theme = useTheme(); const theme = useTheme();
const store = useSettingsStore(); const store = useSettingsStore();
const [isOpen, setIsOpen] = useState(false); const [isOpen, _setIsOpen] = useState(false);
const { t, i18n } = useTranslation('common'); const { t, i18n } = useTranslation('common');
const setLocale = useLocaleUpdateStore(state => state.setLocale); const setLocale = useLocaleUpdateStore(state => state.setLocale);
const firstControlRef = useRef();
const onEsc = e => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
const setIsOpen = open => {
_setIsOpen(open);
if (open) {
window.setTimeout(() => firstControlRef.current.focus(), 150);
document.addEventListener('keyup', onEsc, true);
} else {
document.removeEventListener('keyup', onEsc);
}
};
useEffect(() => { useEffect(() => {
if (Object.keys(locales).includes(i18n.language)) { if (Object.keys(locales).includes(i18n.language)) {
@ -57,7 +75,6 @@ const Settings = () => {
<> <>
<OpenButton <OpenButton
isOpen={isOpen} isOpen={isOpen}
tabIndex="1"
type="button" type="button"
onClick={() => setIsOpen(!isOpen)} title={t('options.name')} onClick={() => setIsOpen(!isOpen)} title={t('options.name')}
> >
@ -78,6 +95,7 @@ const Settings = () => {
}} }}
value={store.weekStart === 0 ? 'Sunday' : 'Monday'} value={store.weekStart === 0 ? 'Sunday' : 'Monday'}
onChange={value => store.setWeekStart(value === 'Sunday' ? 0 : 1)} onChange={value => store.setWeekStart(value === 'Sunday' ? 0 : 1)}
inputRef={firstControlRef}
/> />
<ToggleField <ToggleField

View file

@ -64,12 +64,14 @@ export const Modal = styled.div`
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
transform: translateY(-10px); transform: translateY(-10px);
transition: opacity .15s, transform .15s; visibility: hidden;
transition: opacity .15s, transform .15s, visibility .15s;
${props => props.isOpen && ` ${props => props.isOpen && `
pointer-events: all; pointer-events: all;
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
visibility: visible;
`} `}
`; `;

View file

@ -90,6 +90,17 @@ const TimeRangeField = ({
step = Math.abs(step); step = Math.abs(step);
setStart(step); setStart(step);
}} }}
tabIndex="0"
onKeyDown={e => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
setStart(Math.max(start-1, 0));
}
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
setStart(Math.min(start+1, 24));
}
}}
/> />
<Handle <Handle
value={end} value={end}
@ -113,6 +124,17 @@ const TimeRangeField = ({
step = Math.abs(step); step = Math.abs(step);
setEnd(step); setEnd(step);
}} }}
tabIndex="0"
onKeyDown={e => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
setEnd(Math.max(end-1, 0));
}
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
setEnd(Math.min(end+1, 24));
}
}}
/> />
</Range> </Range>
</Wrapper> </Wrapper>

View file

@ -15,6 +15,7 @@ const ToggleField = ({
options = [], options = [],
value, value,
onChange, onChange,
inputRef,
...props ...props
}) => ( }) => (
<Wrapper> <Wrapper>
@ -30,6 +31,7 @@ const ToggleField = ({
id={`${name}-${label}`} id={`${name}-${label}`}
checked={value === key} checked={value === key}
onChange={() => onChange(key)} onChange={() => onChange(key)}
ref={inputRef}
/> />
<LabelButton htmlFor={`${name}-${label}`}>{label}</LabelButton> <LabelButton htmlFor={`${name}-${label}`}>{label}</LabelButton>
</Option> </Option>

View file

@ -9,9 +9,16 @@ export const ToggleContainer = styled.div`
border: 1px solid ${props => props.theme.primary}; border: 1px solid ${props => props.theme.primary};
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
&:focus-within {
outline: Highlight auto 1px; &:focus-within label {
outline: -webkit-focus-ring-color auto 1px; box-shadow: inset 0 -3px 0 0 var(--focus-color);
}
& > div:first-of-type label {
border-end-start-radius: 2px;
}
& > div:last-of-type label {
border-end-end-radius: 2px;
} }
`; `;
@ -35,6 +42,7 @@ export const HiddenInput = styled.input`
&:checked + label { &:checked + label {
color: ${props => props.theme.background}; color: ${props => props.theme.background};
background-color: ${props => props.theme.primary}; background-color: ${props => props.theme.primary};
--focus-color: ${props => props.theme.primaryDark};
} }
`; `;
@ -48,4 +56,6 @@ export const LabelButton = styled.label`
box-sizing: border-box; box-sizing: border-box;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: box-shadow .15s;
--focus-color: ${props => props.theme.primary};
`; `;

View file

@ -9151,6 +9151,13 @@ react-app-polyfill@^2.0.0:
regenerator-runtime "^0.13.7" regenerator-runtime "^0.13.7"
whatwg-fetch "^3.4.1" whatwg-fetch "^3.4.1"
react-app-rewired@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/react-app-rewired/-/react-app-rewired-2.1.8.tgz#e192f93b98daf96889418d33d3e86cf863812b56"
integrity sha512-wjXPdKPLscA7mn0I1de1NHrbfWdXz4S1ladaGgHVKdn1hTgKK5N6EdGIJM0KrS6bKnJBj7WuqJroDTsPKKr66Q==
dependencies:
semver "^5.6.0"
react-dev-utils@^11.0.3: react-dev-utils@^11.0.3:
version "11.0.3" version "11.0.3"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.3.tgz#b61ed499c7d74f447d4faddcc547e5e671e97c08" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.3.tgz#b61ed499c7d74f447d4faddcc547e5e671e97c08"