summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/components/cell
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-11-04 20:47:52 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-11-16 09:23:09 +0100
commit2ee7b754aa437af74cb4007cd56155fbdd7ba9ff (patch)
treebd07f757e5bf8ab219a87140b3e23b4c5d1b0695 /gui/src/renderer/components/cell
parente10bf00f5cc1d3339402f169df5f0ee37487f771 (diff)
downloadmullvadvpn-2ee7b754aa437af74cb4007cd56155fbdd7ba9ff.tar.xz
mullvadvpn-2ee7b754aa437af74cb4007cd56155fbdd7ba9ff.zip
Move Cell components to dedicated directory
Diffstat (limited to 'gui/src/renderer/components/cell')
-rw-r--r--gui/src/renderer/components/cell/CellButton.tsx44
-rw-r--r--gui/src/renderer/components/cell/Container.tsx29
-rw-r--r--gui/src/renderer/components/cell/Footer.tsx12
-rw-r--r--gui/src/renderer/components/cell/Input.tsx182
-rw-r--r--gui/src/renderer/components/cell/Label.tsx69
-rw-r--r--gui/src/renderer/components/cell/Section.tsx26
-rw-r--r--gui/src/renderer/components/cell/Selector.tsx109
-rw-r--r--gui/src/renderer/components/cell/index.ts6
8 files changed, 477 insertions, 0 deletions
diff --git a/gui/src/renderer/components/cell/CellButton.tsx b/gui/src/renderer/components/cell/CellButton.tsx
new file mode 100644
index 0000000000..a55ffc1153
--- /dev/null
+++ b/gui/src/renderer/components/cell/CellButton.tsx
@@ -0,0 +1,44 @@
+import React, { useContext } from 'react';
+import styled from 'styled-components';
+import { colors } from '../../../config.json';
+import { CellDisabledContext } from './Container';
+import { CellSectionContext } from './Section';
+
+interface IStyledCellButtonProps {
+ selected?: boolean;
+ containedInSection: boolean;
+}
+
+const StyledCellButton = styled.button({}, (props: IStyledCellButtonProps) => ({
+ display: 'flex',
+ padding: '0 16px 0 22px',
+ marginBottom: '1px',
+ flex: 1,
+ alignItems: 'center',
+ alignContent: 'center',
+ cursor: 'default',
+ border: 'none',
+ backgroundColor: props.selected
+ ? colors.green
+ : props.containedInSection
+ ? colors.blue40
+ : colors.blue,
+ ':not(:disabled):hover': {
+ backgroundColor: props.selected ? colors.green : colors.blue80,
+ },
+}));
+
+interface ICellButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+ selected?: boolean;
+}
+
+export const CellButton = styled(
+ React.forwardRef(function Button(props: ICellButtonProps, ref: React.Ref<HTMLButtonElement>) {
+ const containedInSection = useContext(CellSectionContext);
+ return (
+ <CellDisabledContext.Provider value={props.disabled ?? false}>
+ <StyledCellButton ref={ref} containedInSection={containedInSection} {...props} />
+ </CellDisabledContext.Provider>
+ );
+ }),
+)({});
diff --git a/gui/src/renderer/components/cell/Container.tsx b/gui/src/renderer/components/cell/Container.tsx
new file mode 100644
index 0000000000..843180af76
--- /dev/null
+++ b/gui/src/renderer/components/cell/Container.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import styled from 'styled-components';
+import { colors } from '../../../config.json';
+
+const StyledContainer = styled.div({
+ display: 'flex',
+ backgroundColor: colors.blue,
+ alignItems: 'center',
+ paddingLeft: '22px',
+ paddingRight: '16px',
+});
+
+export const CellDisabledContext = React.createContext<boolean>(false);
+
+interface IContainerProps extends React.HTMLAttributes<HTMLDivElement> {
+ disabled?: boolean;
+}
+
+export const Container = React.forwardRef(function ContainerT(
+ props: IContainerProps,
+ ref: React.Ref<HTMLDivElement>,
+) {
+ const { disabled, ...otherProps } = props;
+ return (
+ <CellDisabledContext.Provider value={disabled ?? false}>
+ <StyledContainer ref={ref} {...otherProps} />
+ </CellDisabledContext.Provider>
+ );
+});
diff --git a/gui/src/renderer/components/cell/Footer.tsx b/gui/src/renderer/components/cell/Footer.tsx
new file mode 100644
index 0000000000..2f0dacd10f
--- /dev/null
+++ b/gui/src/renderer/components/cell/Footer.tsx
@@ -0,0 +1,12 @@
+import styled from 'styled-components';
+import { smallText } from '../common-styles';
+
+export const Footer = styled.div({
+ padding: '6px 22px 20px',
+});
+
+export const FooterText = styled.span(smallText);
+
+export const FooterBoldText = styled(FooterText)({
+ fontWeight: 900,
+});
diff --git a/gui/src/renderer/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx
new file mode 100644
index 0000000000..4a11791136
--- /dev/null
+++ b/gui/src/renderer/components/cell/Input.tsx
@@ -0,0 +1,182 @@
+import React, { useCallback, useContext, useState } from 'react';
+import styled from 'styled-components';
+import { colors } from '../../../config.json';
+import { mediumText } from '../common-styles';
+import { CellDisabledContext } from './Container';
+import StandaloneSwitch from '../Switch';
+
+export function Switch(props: StandaloneSwitch['props']) {
+ const disabled = useContext(CellDisabledContext);
+ return <StandaloneSwitch disabled={disabled} {...props} />;
+}
+
+export const InputFrame = styled.div({
+ flexGrow: 0,
+ backgroundColor: 'rgba(255,255,255,0.1)',
+ borderRadius: '4px',
+ padding: '4px 8px',
+});
+
+const inputTextStyles: React.CSSProperties = {
+ ...mediumText,
+ fontWeight: 600,
+ height: '28px',
+ textAlign: 'right',
+ padding: '0px',
+};
+
+const StyledInput = styled.input({}, (props: { valid?: boolean }) => ({
+ ...inputTextStyles,
+ backgroundColor: 'transparent',
+ border: 'none',
+ width: '100%',
+ height: '100%',
+ color: props.valid !== false ? colors.white : colors.red,
+ '::placeholder': {
+ color: colors.white60,
+ },
+}));
+
+const StyledAutoSizingTextInputContainer = styled.div({
+ position: 'relative',
+});
+
+const StyledAutoSizingTextInputFiller = styled.pre({
+ ...inputTextStyles,
+ minWidth: '80px',
+ color: 'transparent',
+});
+
+const StyledAutoSizingTextInputWrapper = styled.div({
+ position: 'absolute',
+ top: '0px',
+ left: '0px',
+ width: '100%',
+ height: '100%',
+});
+
+interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
+ value?: string;
+ validateValue?: (value: string) => boolean;
+ modifyValue?: (value: string) => string;
+ submitOnBlur?: boolean;
+ onSubmitValue?: (value: string) => void;
+ onChangeValue?: (value: string) => void;
+}
+
+interface IInputState {
+ value?: string;
+ focused: boolean;
+}
+
+export class Input extends React.Component<IInputProps, IInputState> {
+ public state = {
+ value: this.props.value ?? '',
+ focused: false,
+ };
+
+ public componentDidUpdate(prevProps: IInputProps, _prevState: IInputState) {
+ if (
+ !this.state.focused &&
+ prevProps.value !== this.props.value &&
+ this.props.value !== this.state.value
+ ) {
+ this.setState(
+ (_state, props) => ({
+ value: props.value,
+ }),
+ () => {
+ this.props.onChangeValue?.(this.state.value);
+ },
+ );
+ }
+ }
+
+ public render() {
+ const {
+ type: _type,
+ onChange: _onChange,
+ onFocus: _onFocus,
+ onBlur: _onBlur,
+ onKeyPress: _onKeyPress,
+ value: _value,
+ modifyValue: _modifyValue,
+ submitOnBlur: _submitOnBlur,
+ onChangeValue: _onChangeValue,
+ onSubmitValue: _onSubmitValue,
+ validateValue,
+ ...otherProps
+ } = this.props;
+
+ const valid = validateValue?.(this.state.value);
+
+ return (
+ <CellDisabledContext.Consumer>
+ {(disabled) => (
+ <StyledInput
+ type="text"
+ valid={valid}
+ aria-invalid={!valid}
+ onChange={this.onChange}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ onKeyPress={this.onKeyPress}
+ value={this.state.value}
+ disabled={disabled}
+ {...otherProps}
+ />
+ )}
+ </CellDisabledContext.Consumer>
+ );
+ }
+
+ private onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const value = this.props.modifyValue?.(event.target.value) ?? event.target.value;
+ this.setState({ value });
+ this.props.onChange?.(event);
+ this.props.onChangeValue?.(value);
+ };
+
+ private onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
+ this.setState({ focused: true });
+ this.props.onFocus?.(event);
+ };
+
+ private onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
+ this.setState({ focused: false });
+ this.props.onBlur?.(event);
+ if (this.props.submitOnBlur) {
+ this.props.onSubmitValue?.(this.state.value);
+ }
+ };
+
+ private onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
+ if (event.key === 'Enter') {
+ this.props.onSubmitValue?.(this.state.value);
+ }
+ this.props.onKeyPress?.(event);
+ };
+}
+
+export function AutoSizingTextInput({ onChangeValue, ...otherProps }: IInputProps) {
+ const [value, setValue] = useState(otherProps.value ?? '');
+
+ const onChangeValueWrapper = useCallback(
+ (value: string) => {
+ setValue(value);
+ onChangeValue?.(value);
+ },
+ [onChangeValue],
+ );
+
+ return (
+ <StyledAutoSizingTextInputContainer>
+ <StyledAutoSizingTextInputWrapper>
+ <Input onChangeValue={onChangeValueWrapper} {...otherProps} />
+ </StyledAutoSizingTextInputWrapper>
+ <StyledAutoSizingTextInputFiller className={otherProps.className} aria-hidden={true}>
+ {value === '' ? otherProps.placeholder : value}
+ </StyledAutoSizingTextInputFiller>
+ </StyledAutoSizingTextInputContainer>
+ );
+}
diff --git a/gui/src/renderer/components/cell/Label.tsx b/gui/src/renderer/components/cell/Label.tsx
new file mode 100644
index 0000000000..12a312049b
--- /dev/null
+++ b/gui/src/renderer/components/cell/Label.tsx
@@ -0,0 +1,69 @@
+import React, { useContext } from 'react';
+import styled from 'styled-components';
+import { colors } from '../../../config.json';
+import { buttonText, smallText } from '../common-styles';
+import ImageView, { IImageViewProps } from '../ImageView';
+import { CellButton } from './CellButton';
+import { CellDisabledContext } from './Container';
+
+const StyledLabel = styled.div(buttonText, (props: { disabled: boolean }) => ({
+ margin: '14px 0',
+ flex: 1,
+ color: props.disabled ? colors.white40 : colors.white,
+ textAlign: 'left',
+}));
+
+const StyledSubText = styled.span(smallText, (props: { disabled: boolean }) => ({
+ color: props.disabled ? colors.white20 : colors.white60,
+ fontWeight: 800,
+ flex: -1,
+ textAlign: 'right',
+ marginLeft: '8px',
+ marginRight: '8px',
+}));
+
+const StyledIconContainer = styled.div((props: { disabled: boolean }) => ({
+ opacity: props.disabled ? 0.4 : 1,
+}));
+
+const StyledTintedIcon = styled(ImageView).attrs((props: IImageViewProps) => ({
+ tintColor: props.tintColor ?? colors.white60,
+ tintHoverColor: props.tintHoverColor ?? props.tintColor ?? colors.white60,
+}))((props: IImageViewProps) => ({
+ [CellButton + ':hover &']: {
+ backgroundColor: props.tintHoverColor,
+ },
+}));
+
+export function Label(props: React.HTMLAttributes<HTMLDivElement>) {
+ const disabled = useContext(CellDisabledContext);
+ return <StyledLabel disabled={disabled} {...props} />;
+}
+
+export function InputLabel(props: React.LabelHTMLAttributes<HTMLLabelElement>) {
+ const disabled = useContext(CellDisabledContext);
+ return <StyledLabel as="label" disabled={disabled} {...props} />;
+}
+
+export function SubText(props: React.HTMLAttributes<HTMLDivElement>) {
+ const disabled = useContext(CellDisabledContext);
+ return <StyledSubText disabled={disabled} {...props} />;
+}
+
+export function UntintedIcon(props: IImageViewProps) {
+ const disabled = useContext(CellDisabledContext);
+ return (
+ <StyledIconContainer disabled={disabled}>
+ <ImageView {...props} />
+ </StyledIconContainer>
+ );
+}
+
+export function Icon(props: IImageViewProps) {
+ const disabled = useContext(CellDisabledContext);
+ return (
+ <StyledIconContainer disabled={disabled}>
+ <StyledTintedIcon {...props} />
+ </StyledIconContainer>
+ );
+}
diff --git a/gui/src/renderer/components/cell/Section.tsx b/gui/src/renderer/components/cell/Section.tsx
new file mode 100644
index 0000000000..25bef54df5
--- /dev/null
+++ b/gui/src/renderer/components/cell/Section.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import styled from 'styled-components';
+import { colors } from '../../../config.json';
+import { buttonText } from '../common-styles';
+
+const StyledSection = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+export const SectionTitle = styled.span(buttonText, {
+ backgroundColor: colors.blue,
+ padding: '14px 16px 14px 22px',
+ marginBottom: '1px',
+});
+
+export const CellSectionContext = React.createContext<boolean>(false);
+
+export function Section(props: React.HTMLAttributes<HTMLDivElement>) {
+ const { children, ...otherProps } = props;
+ return (
+ <StyledSection {...otherProps}>
+ <CellSectionContext.Provider value={true}>{children}</CellSectionContext.Provider>
+ </StyledSection>
+ );
+}
diff --git a/gui/src/renderer/components/cell/Selector.tsx b/gui/src/renderer/components/cell/Selector.tsx
new file mode 100644
index 0000000000..cd308fa3e4
--- /dev/null
+++ b/gui/src/renderer/components/cell/Selector.tsx
@@ -0,0 +1,109 @@
+import * as React from 'react';
+import styled from 'styled-components';
+import { colors } from '../../../config.json';
+import { AriaInput, AriaLabel } from '../AriaGroup';
+import * as Cell from '.';
+
+export interface ISelectorItem<T> {
+ label: string;
+ value: T;
+ disabled?: boolean;
+}
+
+interface ISelectorProps<T> {
+ title?: string;
+ values: Array<ISelectorItem<T>>;
+ value: T;
+ onSelect: (value: T) => void;
+ selectedCellRef?: React.Ref<HTMLButtonElement>;
+ className?: string;
+}
+
+const Section = styled(Cell.Section)({
+ marginBottom: 20,
+});
+
+export default class Selector<T> extends React.Component<ISelectorProps<T>> {
+ public render() {
+ const items = this.props.values.map((item, i) => {
+ const selected = item.value === this.props.value;
+
+ return (
+ <SelectorCell
+ key={i}
+ value={item.value}
+ selected={selected}
+ disabled={item.disabled}
+ forwardedRef={selected ? this.props.selectedCellRef : undefined}
+ onSelect={this.props.onSelect}>
+ {item.label}
+ </SelectorCell>
+ );
+ });
+
+ const title = this.props.title && (
+ <AriaLabel>
+ <Cell.SectionTitle as="label">{this.props.title}</Cell.SectionTitle>
+ </AriaLabel>
+ );
+
+ return (
+ <AriaInput>
+ <Section role="listbox" className={this.props.className}>
+ {title}
+ {items}
+ </Section>
+ </AriaInput>
+ );
+ }
+}
+
+const StyledCellIcon = styled(Cell.Icon)((props: { visible: boolean }) => ({
+ opacity: props.visible ? 1 : 0,
+ marginRight: '8px',
+}));
+
+const StyledLabel = styled(Cell.Label)({
+ fontFamily: 'Open Sans',
+ fontWeight: 'normal',
+ fontSize: '16px',
+});
+
+interface ISelectorCellProps<T> {
+ value: T;
+ selected: boolean;
+ disabled?: boolean;
+ onSelect: (value: T) => void;
+ children?: React.ReactText;
+ forwardedRef?: React.Ref<HTMLButtonElement>;
+}
+
+class SelectorCell<T> extends React.Component<ISelectorCellProps<T>> {
+ public render() {
+ return (
+ <Cell.CellButton
+ ref={this.props.forwardedRef}
+ onClick={this.onClick}
+ selected={this.props.selected}
+ disabled={this.props.disabled}
+ role="option"
+ aria-selected={this.props.selected}
+ aria-disabled={this.props.disabled}>
+ <StyledCellIcon
+ visible={this.props.selected}
+ source="icon-tick"
+ width={24}
+ height={24}
+ tintColor={colors.white}
+ />
+ <StyledLabel>{this.props.children}</StyledLabel>
+ </Cell.CellButton>
+ );
+ }
+
+ private onClick = () => {
+ if (!this.props.selected) {
+ this.props.onSelect(this.props.value);
+ }
+ };
+}
diff --git a/gui/src/renderer/components/cell/index.ts b/gui/src/renderer/components/cell/index.ts
new file mode 100644
index 0000000000..a16cb93097
--- /dev/null
+++ b/gui/src/renderer/components/cell/index.ts
@@ -0,0 +1,6 @@
+export * from './CellButton';
+export * from './Container';
+export * from './Footer';
+export * from './Input';
+export * from './Label';
+export * from './Section';