summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/components/cell
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-11-17 11:09:35 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-11-17 11:09:35 +0100
commit6d95d36bb37e73e65f7b4950993dc3e42d447a5c (patch)
treeee2d4b9f0de9cd08615c396f7157baa603e9194e /gui/src/renderer/components/cell
parent765f777dd4399b334fe6641e5d427d379826501a (diff)
parent6332e991b2bfaf334df03d93d5bd20df06ae699f (diff)
downloadmullvadvpn-6d95d36bb37e73e65f7b4950993dc3e42d447a5c.tar.xz
mullvadvpn-6d95d36bb37e73e65f7b4950993dc3e42d447a5c.zip
Merge branch 'custom-dns-ui'
Diffstat (limited to 'gui/src/renderer/components/cell')
-rw-r--r--gui/src/renderer/components/cell/Input.tsx143
-rw-r--r--gui/src/renderer/components/cell/List.tsx123
2 files changed, 264 insertions, 2 deletions
diff --git a/gui/src/renderer/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx
index bfa47b6687..0ab5b4c493 100644
--- a/gui/src/renderer/components/cell/Input.tsx
+++ b/gui/src/renderer/components/cell/Input.tsx
@@ -1,9 +1,10 @@
-import React, { useCallback, useContext, useState } from 'react';
+import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { colors } from '../../../config.json';
import { mediumText } from '../common-styles';
-import { CellDisabledContext } from './Container';
+import { CellDisabledContext, Container } from './Container';
import StandaloneSwitch from '../Switch';
+import ImageView from '../ImageView';
export const Switch = React.forwardRef(function SwitchT(
props: StandaloneSwitch['props'],
@@ -183,3 +184,141 @@ export function AutoSizingTextInput({ onChangeValue, ...otherProps }: IInputProp
</StyledAutoSizingTextInputContainer>
);
}
+
+const StyledCellInputRowContainer = styled(Container)({
+ backgroundColor: 'white',
+ marginBottom: '1px',
+});
+
+const StyledSubmitButton = styled.button({
+ border: 'none',
+ backgroundColor: 'transparent',
+ padding: '14px 0',
+});
+
+const StyledInputWrapper = styled.div({}, (props: { marginLeft: number }) => ({
+ position: 'relative',
+ flex: 1,
+ width: '171px',
+ marginLeft: props.marginLeft + 'px',
+ marginRight: '25px',
+ lineHeight: '24px',
+ minHeight: '24px',
+ fontFamily: 'Open Sans',
+ fontWeight: 'normal',
+ fontSize: '16px',
+ padding: '14px 0',
+ maxWidth: '100%',
+}));
+
+const StyledTextArea = styled.textarea({}, (props: { invalid?: boolean }) => ({
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ backgroundColor: 'transparent',
+ border: 'none',
+ flex: 1,
+ lineHeight: '24px',
+ fontFamily: 'Open Sans',
+ fontWeight: 'normal',
+ fontSize: '16px',
+ resize: 'none',
+ padding: '14px 0',
+ color: props.invalid ? colors.red : 'auto',
+}));
+
+const StyledInputFiller = styled.div({
+ whiteSpace: 'pre-wrap',
+ overflowWrap: 'break-word',
+ minHeight: '24px',
+ color: 'transparent',
+});
+
+interface IRowInputProps {
+ onChange?: (value: string) => void;
+ onSubmit: (value: string) => void;
+ onFocus?: (event: React.FocusEvent<HTMLTextAreaElement>) => void;
+ onBlur?: (event?: React.FocusEvent<HTMLTextAreaElement>) => void;
+ paddingLeft?: number;
+ invalid?: boolean;
+ autofocus?: boolean;
+}
+
+export function RowInput(props: IRowInputProps) {
+ const [value, setValue] = useState('');
+ const textAreaRef = useRef() as React.RefObject<HTMLTextAreaElement>;
+
+ const submit = useCallback(() => props.onSubmit(value), [props.onSubmit, value]);
+ const onChange = useCallback(
+ (event: React.ChangeEvent<HTMLTextAreaElement>) => {
+ const value = event.target.value;
+ setValue(value);
+ props.onChange?.(value);
+ },
+ [props.onChange],
+ );
+ const onKeyDown = useCallback(
+ (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ submit();
+ }
+ },
+ [submit],
+ );
+
+ const globalKeyListener = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ event.stopPropagation();
+ props.onBlur?.();
+ }
+ },
+ [props.onBlur],
+ );
+
+ useEffect(() => {
+ if (props.autofocus) {
+ textAreaRef.current?.focus();
+ }
+ }, []);
+
+ useEffect(() => {
+ if (props.invalid) {
+ textAreaRef.current?.focus();
+ }
+ }, [props.invalid]);
+
+ useEffect(() => {
+ document.addEventListener('keydown', globalKeyListener, true);
+ return () => document.removeEventListener('keydown', globalKeyListener, true);
+ }, []);
+
+ return (
+ <StyledCellInputRowContainer>
+ <StyledInputWrapper marginLeft={props.paddingLeft ?? 0}>
+ <StyledInputFiller>{value}</StyledInputFiller>
+ <StyledTextArea
+ ref={textAreaRef}
+ onChange={onChange}
+ onKeyDown={onKeyDown}
+ rows={1}
+ value={value}
+ invalid={props.invalid}
+ onFocus={props.onFocus}
+ onBlur={props.onBlur}
+ />
+ </StyledInputWrapper>
+ <StyledSubmitButton onClick={submit}>
+ <ImageView
+ source="icon-tick"
+ height={22}
+ tintColor={colors.green}
+ tintHoverColor={colors.green90}
+ />
+ </StyledSubmitButton>
+ </StyledCellInputRowContainer>
+ );
+}
diff --git a/gui/src/renderer/components/cell/List.tsx b/gui/src/renderer/components/cell/List.tsx
new file mode 100644
index 0000000000..cd99a052cb
--- /dev/null
+++ b/gui/src/renderer/components/cell/List.tsx
@@ -0,0 +1,123 @@
+import React, { useCallback } from 'react';
+import styled from 'styled-components';
+import { colors } from '../../../config.json';
+import { messages } from '../../../shared/gettext';
+import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from '../AriaGroup';
+import ImageView from '../ImageView';
+import * as Cell from '.';
+
+export interface ICellListItem<T> {
+ label: string;
+ value: T;
+}
+
+interface ICellListProps<T> {
+ title?: string;
+ items: Array<ICellListItem<T>>;
+ onSelect?: (value: T) => void;
+ onRemove?: (value: T) => void;
+ className?: string;
+ paddingLeft?: number;
+}
+
+export default function CellList<T>(props: ICellListProps<T>) {
+ const paddingLeft = props.paddingLeft ?? 32;
+
+ return (
+ <Cell.Section role="listbox" className={props.className}>
+ {props.title && <Cell.SectionTitle as="label">{props.title}</Cell.SectionTitle>}
+ {props.items.map((item, i) => {
+ return (
+ <CellListItem
+ key={`${i}-${item.value}`}
+ value={item.value}
+ onSelect={props.onSelect}
+ onRemove={props.onRemove}
+ paddingLeft={paddingLeft}>
+ {item.label}
+ </CellListItem>
+ );
+ })}
+ </Cell.Section>
+ );
+}
+
+const StyledContainer = styled(Cell.Container)({
+ display: 'flex',
+ marginBottom: '1px',
+ backgroundColor: colors.blue40,
+});
+
+const StyledButton = styled.button({
+ display: 'flex',
+ alignItems: 'center',
+ flex: 1,
+ border: 'none',
+ background: 'transparent',
+ padding: 0,
+ margin: 0,
+});
+
+const StyledLabel = styled(Cell.Label)({}, (props: { paddingLeft: number }) => ({
+ fontFamily: 'Open Sans',
+ fontWeight: 'normal',
+ fontSize: '16px',
+ paddingLeft: props.paddingLeft + 'px',
+ whiteSpace: 'pre-wrap',
+ overflowWrap: 'break-word',
+ width: '171px',
+ marginRight: '25px',
+}));
+
+const StyledRemoveButton = styled.button({
+ background: 'transparent',
+ border: 'none',
+ padding: 0,
+});
+
+const StyledRemoveIcon = styled(ImageView)({
+ [StyledRemoveButton + ':hover &']: {
+ backgroundColor: colors.white80,
+ },
+});
+
+interface ICellListItemProps<T> {
+ value: T;
+ onSelect?: (application: T) => void;
+ onRemove?: (application: T) => void;
+ paddingLeft: number;
+ children: string;
+}
+
+function CellListItem<T>(props: ICellListItemProps<T>) {
+ const onSelect = useCallback(() => props.onSelect?.(props.value), [props.onSelect, props.value]);
+ const onRemove = useCallback(() => props.onRemove?.(props.value), [props.onRemove, props.value]);
+
+ return (
+ <AriaDescriptionGroup>
+ <StyledContainer>
+ <StyledButton
+ onClick={props.onSelect ? onSelect : undefined}
+ as={props.onSelect ? 'button' : 'span'}>
+ <AriaDescription>
+ <StyledLabel paddingLeft={props.paddingLeft}>{props.children}</StyledLabel>
+ </AriaDescription>
+ </StyledButton>
+ {props.onRemove && (
+ <AriaDescribed>
+ <StyledRemoveButton
+ onClick={onRemove}
+ aria-label={messages.pgettext('accessibility', 'Remove item')}>
+ <StyledRemoveIcon
+ source="icon-close"
+ width={22}
+ height={22}
+ tintColor={colors.white60}
+ />
+ </StyledRemoveButton>
+ </AriaDescribed>
+ )}
+ </StyledContainer>
+ </AriaDescriptionGroup>
+ );
+}