diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-11-04 20:47:52 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-11-16 09:23:09 +0100 |
| commit | 2ee7b754aa437af74cb4007cd56155fbdd7ba9ff (patch) | |
| tree | bd07f757e5bf8ab219a87140b3e23b4c5d1b0695 /gui/src/renderer/components/cell | |
| parent | e10bf00f5cc1d3339402f169df5f0ee37487f771 (diff) | |
| download | mullvadvpn-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.tsx | 44 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Container.tsx | 29 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Footer.tsx | 12 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Input.tsx | 182 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Label.tsx | 69 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Section.tsx | 26 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Selector.tsx | 109 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/index.ts | 6 |
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'; |
