diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-11-17 11:09:35 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-11-17 11:09:35 +0100 |
| commit | 6d95d36bb37e73e65f7b4950993dc3e42d447a5c (patch) | |
| tree | ee2d4b9f0de9cd08615c396f7157baa603e9194e /gui/src/renderer/components/cell | |
| parent | 765f777dd4399b334fe6641e5d427d379826501a (diff) | |
| parent | 6332e991b2bfaf334df03d93d5bd20df06ae699f (diff) | |
| download | mullvadvpn-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.tsx | 143 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/List.tsx | 123 |
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> + ); +} |
