diff options
Diffstat (limited to 'src')
33 files changed, 771 insertions, 680 deletions
diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index a0e22e4..1c673fa 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -1,4 +1,3 @@ -import "./console.scss"; import { connect } from "react-redux"; import React from "react"; import Input from "./console/Input"; @@ -11,6 +10,13 @@ import CommandLineParser, { } from "../commandline/CommandLineParser"; import { Command } from "../../shared/Command"; import ColorScheme from "../../shared/ColorScheme"; +import { LightTheme, DarkTheme } from "./Theme"; +import styled from "./Theme"; +import { ThemeProvider } from "styled-components"; + +const ConsoleWrapper = styled.div` + border-top: 1px solid gray; +`; const COMPLETION_MAX_ITEMS = 33; @@ -143,28 +149,34 @@ class Console extends React.Component<Props> { case "command": case "find": return ( - <div data-theme={theme} className="vimvixen-console-command-wrapper"> - <Completion - size={COMPLETION_MAX_ITEMS} - completions={this.props.completions} - select={this.props.select} - /> - <Input - ref={this.input} - mode={this.props.mode} - onBlur={this.onBlur.bind(this)} - onKeyDown={this.onKeyDown.bind(this)} - onChange={this.onChange.bind(this)} - value={this.props.consoleText} - /> - </div> + <ThemeProvider + theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme} + > + <ConsoleWrapper> + <Completion + size={COMPLETION_MAX_ITEMS} + completions={this.props.completions} + select={this.props.select} + /> + <Input + ref={this.input} + mode={this.props.mode} + onBlur={this.onBlur.bind(this)} + onKeyDown={this.onKeyDown.bind(this)} + onChange={this.onChange.bind(this)} + value={this.props.consoleText} + /> + </ConsoleWrapper> + </ThemeProvider> ); case "info": case "error": return ( - <div data-theme={theme}> + <ThemeProvider + theme={theme === ColorScheme.Dark ? DarkTheme : LightTheme} + > <Message mode={this.props.mode}>{this.props.messageText}</Message> - </div> + </ThemeProvider> ); default: return null; diff --git a/src/console/components/Theme.ts b/src/console/components/Theme.ts new file mode 100644 index 0000000..dd7baa5 --- /dev/null +++ b/src/console/components/Theme.ts @@ -0,0 +1,53 @@ +import baseStyled, { ThemedStyledInterface } from "styled-components"; + +type Theme = { + completionTitleBackground: string; + completionTitleForeground: string; + completionItemBackground: string; + completionItemForeground: string; + completionItemDescriptionForeground: string; + completionSelectedBackground: string; + completionSelectedForeground: string; + commandBackground: string; + commandForeground: string; + consoleErrorBackground: string; + consoleErrorForeground: string; + consoleInfoBackground: string; + consoleInfoForeground: string; +}; + +export const LightTheme: Theme = { + completionTitleBackground: "lightgray", + completionTitleForeground: "#000000", + completionItemBackground: "#ffffff", + completionItemForeground: "#000000", + completionItemDescriptionForeground: "#008000", + completionSelectedBackground: "#ffff00", + completionSelectedForeground: "#000000", + commandBackground: "#ffffff", + commandForeground: "#000000", + consoleErrorBackground: "#ff0000", + consoleErrorForeground: "#ffffff", + consoleInfoBackground: "#ffffff", + consoleInfoForeground: "#018786", +}; + +export const DarkTheme: Theme = { + completionTitleBackground: "#052027", + completionTitleForeground: "white", + completionItemBackground: "#2f474f", + completionItemForeground: "white", + completionItemDescriptionForeground: "#86fab0", + completionSelectedBackground: "#eeff41", + completionSelectedForeground: "#000000", + commandBackground: "#052027", + commandForeground: "white", + consoleErrorBackground: "red", + consoleErrorForeground: "white", + consoleInfoBackground: "#052027", + consoleInfoForeground: "#ffffff", +}; + +const styled = baseStyled as ThemedStyledInterface<Theme>; + +export default styled; diff --git a/src/console/components/console.scss b/src/console/components/console.scss deleted file mode 100644 index ccb769b..0000000 --- a/src/console/components/console.scss +++ /dev/null @@ -1,141 +0,0 @@ -[data-theme="light"] { - --completion-title-background: lightgray; - --completion-title-foreground: #000000; - --completion-item-background: #ffffff; - --completion-item-foreground: #000000; - --completion-item-description-foreground: #008000; - --completion-selected-background: #ffff00; - --completion-selected-foreground: #000000; - --command-background: #ffffff; - --command-foreground: #000000; - --console-error-background: #ff0000; - --console-error-foreground: #ffffff; - --console-info-background: #ffffff; - --console-info-foreground: #018786; -} - -[data-theme="dark"] { - --completion-title-background: #052027; - --completion-title-foreground: white; - --completion-item-background: #2f474f; - --completion-item-foreground: white; - --completion-item-description-foreground: #86fab0; - --completion-selected-background: #eeff41; - --completion-selected-foreground: #000000; - --command-background: #052027; - --command-foreground: white; - --console-error-background: red; - --console-error-foreground: white; - --console-info-background: #052027; - --console-info-foreground: #ffffff; -} - -html, body, * { - margin: 0; - padding: 0; -} - -body { - position: absolute; - bottom: 0; - left: 0; - right: 0; - overflow: hidden; -} - -.vimvixen-console { - bottom: 0; - margin: 0; - padding: 0; - - @mixin console-font { - font-style: normal; - font-family: monospace; - font-size: 12px; - line-height: 16px; - } - - &-command-wrapper { - border-top: 1px solid gray; - } - - &-completion { - @include console-font; - - &-title { - background-color: var(--completion-title-background); - color: var(--completion-title-foreground); - font-weight: bold; - margin: 0; - padding: 0; - } - - &-item { - background-color: var(--completion-item-background); - color: var(--completion-item-foreground); - - padding-left: 1.5rem; - background-position: 0 center; - background-size: contain; - background-repeat: no-repeat; - white-space: pre; - - &.vimvixen-completion-selected { - background-color: var(--completion-selected-background); - color: var(--completion-selected-foreground); - } - - &-caption { - display: inline-block; - width: 40%; - text-overflow: ellipsis; - overflow: hidden; - } - - &-url { - display: inline-block; - color: var(--completion-item-description-foreground); - width: 60%; - text-overflow: ellipsis; - overflow: hidden; - } - } - } - - &-message { - @include console-font; - - border-top: 1px solid gray; - } - - &-error { - background-color: var(--console-error-background); - color: var(--console-error-foreground); - font-weight: bold; - } - - &-info { - background-color: var(--console-info-background); - color: var(--console-info-foreground); - font-weight: normal; - } - - &-command { - background-color: var(--command-background); - color: var(--command-foreground); - display: flex; - - &-prompt { - @include console-font; - } - - &-input { - border: none; - flex-grow: 1; - background-color: var(--command-background); - color: var(--command-foreground); - - @include console-font; - } - } -} diff --git a/src/console/components/console/Completion.tsx b/src/console/components/console/Completion.tsx index 9b4cf15..09ae278 100644 --- a/src/console/components/console/Completion.tsx +++ b/src/console/components/console/Completion.tsx @@ -63,29 +63,52 @@ class Completion extends React.Component<Props, State> { } render() { - let eles = []; - let index = 0; + let itemIndex = 0; + let viewIndex = 0; + const groups: Array<JSX.Element> = []; + const viewOffset = this.state.viewOffset; + const viewSize = this.props.size; - for (const group of this.props.completions) { - eles.push(<CompletionTitle key={`group-${index}`} title={group.name} />); + this.props.completions.forEach((group, groupIndex) => { + const items = []; + const title = ( + <CompletionTitle + id={`title-${groupIndex}`} + key={`group-${groupIndex}`} + shown={viewOffset <= viewIndex && viewIndex < viewOffset + viewSize} + title={group.name} + /> + ); + ++viewIndex; for (const item of group.items) { - eles.push( + items.push( <CompletionItem - key={`item-${index}`} + shown={viewOffset <= viewIndex && viewIndex < viewOffset + viewSize} + key={`item-${itemIndex}`} icon={item.icon} caption={item.caption} url={item.url} - highlight={index === this.props.select} + highlight={itemIndex === this.props.select} + aria-selected={itemIndex === this.props.select} + role="menuitem" /> ); - ++index; + ++viewIndex; + ++itemIndex; } - } - - const viewOffset = this.state.viewOffset; - eles = eles.slice(viewOffset, viewOffset + this.props.size); + groups.push( + <div + key={`group-${groupIndex}`} + role="group" + aria-describedby={`title-${groupIndex}`} + > + {title} + <ul>{items}</ul> + </div> + ); + }); - return <ul className="vimvixen-console-completion">{eles}</ul>; + return <div role="menu">{groups}</div>; } } diff --git a/src/console/components/console/CompletionItem.tsx b/src/console/components/console/CompletionItem.tsx index 657f360..5f2f9f6 100644 --- a/src/console/components/console/CompletionItem.tsx +++ b/src/console/components/console/CompletionItem.tsx @@ -1,35 +1,62 @@ import React from "react"; -import PropTypes from "prop-types"; +import styled from "../Theme"; + +const Container = styled.li<{ + shown: boolean; + icon: string; + highlight: boolean; +}>` + background-image: ${({ icon }) => "url(" + icon + ")"}; + background-color: ${({ highlight, theme }) => + highlight + ? theme.completionSelectedBackground + : theme.completionItemBackground}; + color: ${({ highlight, theme }) => + highlight + ? theme.completionSelectedForeground + : theme.completionItemForeground}; + display: ${({ shown }) => (shown ? "display" : "none")}; + padding-left: 1.8rem; + background-position: 0 center; + background-size: contain; + background-repeat: no-repeat; + white-space: pre; +`; + +const Caption = styled.span` + display: inline-block; + width: 40%; + text-overflow: ellipsis; + overflow: hidden; +`; + +const Description = styled.span` + display: inline-block; + color: ${({ theme }) => theme.completionItemDescriptionForeground}; + width: 60%; + text-overflow: ellipsis; + overflow: hidden; +`; interface Props { + shown: boolean; highlight: boolean; caption?: string; url?: string; icon?: string; } -const CompletionItem = (props: Props) => { - let className = "vimvixen-console-completion-item"; - if (props.highlight) { - className += " vimvixen-completion-selected"; - } - return ( - <li - className={className} - style={{ backgroundImage: "url(" + props.icon + ")" }} - > - <span className="vimvixen-console-completion-item-caption"> - {props.caption} - </span> - <span className="vimvixen-console-completion-item-url">{props.url}</span> - </li> - ); -}; - -CompletionItem.propTypes = { - highlight: PropTypes.bool, - caption: PropTypes.string, - url: PropTypes.string, -}; +const CompletionItem: React.FC<React.HTMLAttributes<HTMLElement> & Props> = ( + props +) => ( + <Container + icon={props.icon || ""} + aria-labelledby={`completion-item-${props.caption}`} + {...props} + > + <Caption id={`completion-item-${props.caption}`}>{props.caption}</Caption> + <Description>{props.url}</Description> + </Container> +); export default CompletionItem; diff --git a/src/console/components/console/CompletionTitle.tsx b/src/console/components/console/CompletionTitle.tsx index 7257006..ec2fc8b 100644 --- a/src/console/components/console/CompletionTitle.tsx +++ b/src/console/components/console/CompletionTitle.tsx @@ -1,11 +1,22 @@ import React from "react"; +import styled from "../Theme"; + +const Li = styled.li<{ shown: boolean }>` + display: ${({ shown }) => (shown ? "display" : "none")}; + background-color: ${({ theme }) => theme.completionTitleBackground}; + color: ${({ theme }) => theme.completionTitleForeground}; + font-weight: bold; + margin: 0; + padding: 0; +`; interface Props { + shown: boolean; title: string; } -const CompletionTitle = (props: Props) => { - return <li className="vimvixen-console-completion-title">{props.title}</li>; -}; +const CompletionTitle: React.FC<React.HTMLAttributes<HTMLElement> & Props> = ( + props +) => <Li {...props}>{props.title}</Li>; export default CompletionTitle; diff --git a/src/console/components/console/Input.tsx b/src/console/components/console/Input.tsx index e412a0c..448b096 100644 --- a/src/console/components/console/Input.tsx +++ b/src/console/components/console/Input.tsx @@ -1,4 +1,22 @@ import React from "react"; +import styled from "../Theme"; + +const Container = styled.div` + background-color: ${({ theme }) => theme.commandBackground}; + color: ${({ theme }) => theme.commandForeground}; + display: flex; +`; + +const Prompt = styled.i` + font-style: normal; +`; + +const InputInner = styled.input` + border: none; + flex-grow: 1; + background-color: ${({ theme }) => theme.commandBackground}; + color: ${({ theme }) => theme.commandForeground}; +`; interface Props { mode: string; @@ -32,17 +50,16 @@ class Input extends React.Component<Props> { } return ( - <div className="vimvixen-console-command"> - <i className="vimvixen-console-command-prompt">{prompt}</i> - <input - className="vimvixen-console-command-input" + <Container> + <Prompt>{prompt}</Prompt> + <InputInner ref={this.input} onBlur={this.props.onBlur} onKeyDown={this.props.onKeyDown} onChange={this.props.onChange} value={this.props.value} /> - </div> + </Container> ); } } diff --git a/src/console/components/console/Message.tsx b/src/console/components/console/Message.tsx index fd1c9d7..73498fd 100644 --- a/src/console/components/console/Message.tsx +++ b/src/console/components/console/Message.tsx @@ -1,24 +1,31 @@ import React from "react"; +import styled from "../Theme"; + +const Error = styled.p` + border-top: 1px solid gray; + background-color: ${({ theme }) => theme.consoleErrorBackground}; + color: ${({ theme }) => theme.consoleErrorForeground}; + font-weight: bold; +`; + +const Info = styled.p` + border-top: 1px solid gray; + background-color: ${({ theme }) => theme.consoleInfoBackground}; + color: ${({ theme }) => theme.consoleInfoForeground}; + font-weight: normal; +`; interface Props { mode: string; children: string; } -const Message = (props: Props) => { - switch (props.mode) { +const Message: React.FC<Props> = ({ mode, children }) => { + switch (mode) { case "error": - return ( - <p className="vimvixen-console-message vimvixen-console-error"> - {props.children} - </p> - ); + return <Error role="alert">{children}</Error>; case "info": - return ( - <p className="vimvixen-console-message vimvixen-console-info"> - {props.children} - </p> - ); + return <Info role="status">{children}</Info>; } return null; }; diff --git a/src/console/index.css b/src/console/index.css new file mode 100644 index 0000000..2d548df --- /dev/null +++ b/src/console/index.css @@ -0,0 +1,30 @@ +html, body, * { + margin: 0; + padding: 0; + + font-style: normal; + font-family: monospace; + font-size: 12px; + line-height: 16px; +} + +input { + font-style: normal; + font-family: monospace; + font-size: 12px; + line-height: 16px; +} + +body { + position: absolute; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; +} + +.vimvixen-console { + bottom: 0; + margin: 0; + padding: 0; +} diff --git a/src/console/index.tsx b/src/console/index.tsx index e1a9dd3..228625e 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -5,6 +5,7 @@ import promise from "redux-promise"; import * as consoleActions from "./actions/console"; import { Provider } from "react-redux"; import Console from "./components/Console"; +import "./index.css"; import React from "react"; import ReactDOM from "react-dom"; diff --git a/src/content/presenters/FocusPresenter.ts b/src/content/presenters/FocusPresenter.ts index 4d70a6e..3591039 100644 --- a/src/content/presenters/FocusPresenter.ts +++ b/src/content/presenters/FocusPresenter.ts @@ -11,7 +11,7 @@ export class FocusPresenterImpl implements FocusPresenter { .map((type) => `input[type=${type}]`) .join(","); const targets = window.document.querySelectorAll( - inputSelector + ",textarea" + inputSelector + ",input:not([type]),textarea" ); const target = Array.from(targets).find(doms.isVisible); if (target instanceof HTMLInputElement) { diff --git a/src/content/site-style.ts b/src/content/site-style.ts index 0c335fc..3748c6b 100644 --- a/src/content/site-style.ts +++ b/src/content/site-style.ts @@ -8,8 +8,8 @@ export default ` height: 100%; position: fixed; z-index: 2147483647; - border: none; - background-color: unset; + border: none !important; + background-color: unset !important; pointer-events:none; } diff --git a/src/settings/components/form/BlacklistForm.scss b/src/settings/components/form/BlacklistForm.scss deleted file mode 100644 index a230d0d..0000000 --- a/src/settings/components/form/BlacklistForm.scss +++ /dev/null @@ -1,9 +0,0 @@ -.form-blacklist-form { - &-row { - display: flex; - - .column-url { - flex: 1; - } - } -} diff --git a/src/settings/components/form/BlacklistForm.tsx b/src/settings/components/form/BlacklistForm.tsx index 859cb9f..6fb9eca 100644 --- a/src/settings/components/form/BlacklistForm.tsx +++ b/src/settings/components/form/BlacklistForm.tsx @@ -1,9 +1,29 @@ -import "./BlacklistForm.scss"; +import styled from "styled-components"; import AddButton from "../ui/AddButton"; import DeleteButton from "../ui/DeleteButton"; import React from "react"; import Blacklist, { BlacklistItem } from "../../../shared/settings/Blacklist"; +const Grid = styled.div``; + +const GridRow = styled.div` + display: flex; +`; + +const GridCell = styled.div<{ grow?: number }>` + &:nth-child(1) { + flex-grow: 1; + } + &:nth-child(2) { + flex-shrink: 1; + } +`; + +const Input = styled.input` + width: 100%; + box-sizing: border-box; +`; + interface Props { value: Blacklist; onChange: (value: Blacklist) => void; @@ -19,37 +39,46 @@ class BlacklistForm extends React.Component<Props> { render() { return ( - <div className="form-blacklist-form"> - {this.props.value.items.map((item, index) => { - if (item.partial) { - return null; - } - return ( - <div key={index} className="form-blacklist-form-row"> - <input - data-index={index} - type="text" - name="url" - className="column-url" - value={item.pattern} - onChange={this.bindValue.bind(this)} - onBlur={this.props.onBlur} - /> - <DeleteButton - data-index={index} - name="delete" - onClick={this.bindValue.bind(this)} - onBlur={this.props.onBlur} - /> - </div> - ); - })} + <> + <Grid role="list"> + {this.props.value.items.map((item, index) => { + if (item.partial) { + return null; + } + return ( + <GridRow role="listitem" key={index}> + <GridCell> + <Input + data-index={index} + type="text" + name="url" + aria-label="URL" + value={item.pattern} + placeholder="example.com/mail/*" + onChange={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + /> + </GridCell> + <GridCell> + <DeleteButton + data-index={index} + name="delete" + onClick={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + aria-label="Delete" + /> + </GridCell> + </GridRow> + ); + })} + </Grid> <AddButton name="add" + aria-label="Add" style={{ float: "right" }} onClick={this.bindValue.bind(this)} /> - </div> + </> ); } diff --git a/src/settings/components/form/KeymapsForm.scss b/src/settings/components/form/KeymapsForm.scss deleted file mode 100644 index 1a4e5cd..0000000 --- a/src/settings/components/form/KeymapsForm.scss +++ /dev/null @@ -1,11 +0,0 @@ -.form-keymaps-form { - column-count: 3; - - &-field-group { - margin-top: 24px; - } - - &-field-group:first-of-type { - margin-top: 24px; - } -} diff --git a/src/settings/components/form/KeymapsForm.tsx b/src/settings/components/form/KeymapsForm.tsx index b9af0df..6582529 100644 --- a/src/settings/components/form/KeymapsForm.tsx +++ b/src/settings/components/form/KeymapsForm.tsx @@ -1,9 +1,21 @@ -import "./KeymapsForm.scss"; import React from "react"; -import Input from "../ui/Input"; +import styled from "styled-components"; +import Text from "../ui/Text"; import keymaps from "../../keymaps"; import { FormKeymaps } from "../../../shared/SettingData"; +const Grid = styled.div` + column-count: 3; +`; + +const FieldGroup = styled.div` + margin-top: 24px; + + &:first-of-type { + margin-top: 24px; + } +`; + interface Props { value: FormKeymaps; onChange: (e: FormKeymaps) => void; @@ -20,15 +32,14 @@ class KeymapsForm extends React.Component<Props> { render() { const values = this.props.value.toJSON(); return ( - <div className="form-keymaps-form"> + <Grid> {keymaps.fields.map((group, index) => { return ( - <div key={index} className="form-keymaps-form-field-group"> + <FieldGroup key={index}> {group.map(([name, label]) => { const value = values[name] || ""; return ( - <Input - type="text" + <Text id={name} name={name} key={name} @@ -39,10 +50,10 @@ class KeymapsForm extends React.Component<Props> { /> ); })} - </div> + </FieldGroup> ); })} - </div> + </Grid> ); } diff --git a/src/settings/components/form/PartialBlacklistForm.scss b/src/settings/components/form/PartialBlacklistForm.scss deleted file mode 100644 index caf6f93..0000000 --- a/src/settings/components/form/PartialBlacklistForm.scss +++ /dev/null @@ -1,28 +0,0 @@ -.form-partial-blacklist-form { - @mixin row-base { - display: flex; - - .column-url { - flex: 5; - min-width: 0; - } - .column-keys { - flex: 1; - min-width: 0; - } - .column-delete { - flex: 1; - min-width: 0; - } - } - - &-header { - @include row-base; - - font-weight: bold; - } - - &-row { - @include row-base; - } -} diff --git a/src/settings/components/form/PartialBlacklistForm.tsx b/src/settings/components/form/PartialBlacklistForm.tsx index 95beee8..b2da2bb 100644 --- a/src/settings/components/form/PartialBlacklistForm.tsx +++ b/src/settings/components/form/PartialBlacklistForm.tsx @@ -1,9 +1,41 @@ -import "./PartialBlacklistForm.scss"; +import React from "react"; +import styled from "styled-components"; import AddButton from "../ui/AddButton"; import DeleteButton from "../ui/DeleteButton"; -import React from "react"; import Blacklist, { BlacklistItem } from "../../../shared/settings/Blacklist"; +const Grid = styled.div``; + +const GridHeader = styled.div` + display: flex; + font-weight: bold; +`; + +const GridRow = styled.div` + display: flex; +`; + +const GridCell = styled.div<{ grow?: number }>` + &:nth-child(1) { + flex-grow: 5; + } + + &:nth-child(2) { + flex-shrink: 1; + min-width: 20%; + max-width: 20%; + } + + &:nth-child(3) { + flex-shrink: 1; + } +`; + +const Input = styled.input` + width: 100%; + box-sizing: border-box; +`; + interface Props { value: Blacklist; onChange: (value: Blacklist) => void; @@ -19,50 +51,62 @@ class PartialBlacklistForm extends React.Component<Props> { render() { return ( - <div className="form-partial-blacklist-form"> - <div className="form-partial-blacklist-form-header"> - <div className="column-url">URL</div> - <div className="column-keys">Keys</div> - </div> - {this.props.value.items.map((item, index) => { - if (!item.partial) { - return null; - } - return ( - <div key={index} className="form-partial-blacklist-form-row"> - <input - data-index={index} - type="text" - name="url" - className="column-url" - value={item.pattern} - onChange={this.bindValue.bind(this)} - onBlur={this.props.onBlur} - /> - <input - data-index={index} - type="text" - name="keys" - className="column-keys" - value={item.keys.join(",")} - onChange={this.bindValue.bind(this)} - onBlur={this.props.onBlur} - /> - <DeleteButton - data-index={index} - name="delete" - onClick={this.bindValue.bind(this)} - onBlur={this.props.onBlur} - /> - </div> - ); - })} + <> + <Grid role="list"> + <GridHeader> + <GridCell>URL</GridCell> + <GridCell>Keys</GridCell> + </GridHeader> + {this.props.value.items.map((item, index) => { + if (!item.partial) { + return null; + } + return ( + <GridRow key={index} role="listitem"> + <GridCell> + <Input + data-index={index} + type="text" + name="url" + aria-label="URL" + value={item.pattern} + placeholder="example.com/mail/*" + onChange={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + /> + </GridCell> + <GridCell> + <Input + data-index={index} + type="text" + name="keys" + aria-label="Keys" + value={item.keys.join(",")} + placeholder="j,k,<C-d>,<C-u>" + onChange={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + /> + </GridCell> + <GridCell> + <DeleteButton + data-index={index} + name="delete" + aria-label="Delete" + onClick={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + /> + </GridCell> + </GridRow> + ); + })} + </Grid> <AddButton name="add" + aria-label="Add" style={{ float: "right" }} onClick={this.bindValue.bind(this)} /> - </div> + </> ); } diff --git a/src/settings/components/form/PropertiesForm.scss b/src/settings/components/form/PropertiesForm.scss deleted file mode 100644 index 7c9e167..0000000 --- a/src/settings/components/form/PropertiesForm.scss +++ /dev/null @@ -1,12 +0,0 @@ -.form-properties-form { - &-row { - .column-name { - display: inline-block; - min-width: 5rem; - font-weight: bold; - } - .column-input { - line-height: 2.2rem; - } - } -} diff --git a/src/settings/components/form/PropertiesForm.tsx b/src/settings/components/form/PropertiesForm.tsx index aebd9b1..53ebf03 100644 --- a/src/settings/components/form/PropertiesForm.tsx +++ b/src/settings/components/form/PropertiesForm.tsx @@ -1,6 +1,20 @@ -import "./PropertiesForm.scss"; +import styled from "styled-components"; import React from "react"; +const Form = styled.div``; + +const Row = styled.div``; + +const Label = styled.label` + display: inline-block; + min-width: 5rem; + font-weight: bold; +`; + +const Input = styled.input` + line-height: 2.2rem; +`; + interface Props { types: { [key: string]: string }; value: { [key: string]: any }; @@ -21,7 +35,7 @@ class PropertiesForm extends React.Component<Props> { const values = this.props.value; return ( - <div className="form-properties-form"> + <Form> {Object.keys(types).map((name) => { const type = types[name]; let inputType = ""; @@ -42,23 +56,22 @@ class PropertiesForm extends React.Component<Props> { return null; } return ( - <div key={name} className="form-properties-form-row"> - <label> - <span className="column-name">{name}</span> - <input + <Row key={name}> + <Label> + <span>{name}</span> + <Input type={inputType} name={name} - className="column-input" value={values[name] ? values[name] : ""} onChange={onChange} onBlur={this.props.onBlur} checked={values[name]} /> - </label> - </div> + </Label> + </Row> ); })} - </div> + </Form> ); } diff --git a/src/settings/components/form/SearchForm.scss b/src/settings/components/form/SearchForm.scss deleted file mode 100644 index 26b2f44..0000000 --- a/src/settings/components/form/SearchForm.scss +++ /dev/null @@ -1,28 +0,0 @@ -.form-search-form { - @mixin row-base { - display: flex; - - .column-name { - flex: 1; - min-width: 0; - } - .column-url { - flex: 5; - min-width: 0; - } - .column-option { - text-align: right; - flex-basis: 5rem; - } - } - - &-header { - @include row-base; - - font-weight: bold; - } - - &-row { - @include row-base; - } -} diff --git a/src/settings/components/form/SearchForm.tsx b/src/settings/components/form/SearchForm.tsx index a4d923d..4bf0e02 100644 --- a/src/settings/components/form/SearchForm.tsx +++ b/src/settings/components/form/SearchForm.tsx @@ -1,9 +1,42 @@ -import "./SearchForm.scss"; import React from "react"; +import styled from "styled-components"; import AddButton from "../ui/AddButton"; import DeleteButton from "../ui/DeleteButton"; import { FormSearch } from "../../../shared/SettingData"; +const Grid = styled.div``; + +const GridHeader = styled.div` + display: flex; + font-weight: bold; +`; + +const GridRow = styled.div` + display: flex; +`; + +const GridCell = styled.div<{ grow?: number }>` + &:nth-child(1) { + flex-grow: 0; + min-width: 10%; + max-width: 10%; + } + + &:nth-child(2) { + flex-grow: 2; + } + + &:nth-child(3) { + flex-grow: 0; + flex-shrink: 1; + } +`; + +const Input = styled.input` + width: 100%; + box-sizing: border-box; +`; + interface Props { value: FormSearch; onChange: (value: FormSearch) => void; @@ -20,57 +53,67 @@ class SearchForm extends React.Component<Props> { render() { const value = this.props.value.toJSON(); return ( - <div className="form-search-form"> - <div className="form-search-form-header"> - <div className="column-name">Name</div> - <div className="column-url">URL</div> - <div className="column-option">Default</div> - </div> - {value.engines.map((engine, index) => { - return ( - <div key={index} className="form-search-form-row"> - <input - data-index={index} - type="text" - name="name" - className="column-name" - value={engine[0]} - onChange={this.bindValue.bind(this)} - onBlur={this.props.onBlur} - /> - <input - data-index={index} - type="text" - name="url" - placeholder="http://example.com/?q={}" - className="column-url" - value={engine[1]} - onChange={this.bindValue.bind(this)} - onBlur={this.props.onBlur} - /> - <div className="column-option"> - <input - data-index={index} - type="radio" - name="default" - checked={value.default === engine[0]} - onChange={this.bindValue.bind(this)} - /> - <DeleteButton - data-index={index} - name="delete" - onClick={this.bindValue.bind(this)} - /> - </div> - </div> - ); - })} + <> + <Grid role="list"> + <GridHeader> + <GridCell>Name</GridCell> + <GridCell>URL</GridCell> + <GridCell>Default</GridCell> + </GridHeader> + {value.engines.map((engine, index) => { + return ( + <GridRow key={index} role="listitem"> + <GridCell> + <Input + data-index={index} + type="text" + name="name" + aria-label="Name" + value={engine[0]} + onChange={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + /> + </GridCell> + <GridCell> + <Input + data-index={index} + type="text" + name="url" + aria-label="URL" + placeholder="http://example.com/?q={}" + value={engine[1]} + onChange={this.bindValue.bind(this)} + onBlur={this.props.onBlur} + /> + </GridCell> + <GridCell> + <input + data-index={index} + type="radio" + name="default" + aria-label="Default" + checked={value.default === engine[0]} + onChange={this.bindValue.bind(this)} + /> + a + <DeleteButton + data-index={index} + aria-label="Delete" + name="delete" + onClick={this.bindValue.bind(this)} + /> + </GridCell> + </GridRow> + ); + })} + </Grid> <AddButton name="add" + aria-label="Add" style={{ float: "right" }} onClick={this.bindValue.bind(this)} /> - </div> + </> ); } diff --git a/src/settings/components/index.tsx b/src/settings/components/index.tsx index 9d71cac..2e2ff52 100644 --- a/src/settings/components/index.tsx +++ b/src/settings/components/index.tsx @@ -1,7 +1,8 @@ -import "./site.scss"; import React from "react"; import { connect } from "react-redux"; -import Input from "./ui/Input"; +import styled from "styled-components"; +import TextArea from "./ui/TextArea"; +import Radio from "./ui/Radio"; import SearchForm from "./form/SearchForm"; import KeymapsForm from "./form/KeymapsForm"; import BlacklistForm from "./form/BlacklistForm"; @@ -18,6 +19,28 @@ import { State as AppState } from "../reducers/setting"; import Properties from "../../shared/settings/Properties"; import Blacklist from "../../shared/settings/Blacklist"; +const Container = styled.form` + padding: 2px; + font-family: system-ui; +`; + +const Fieldset = styled.fieldset` + margin: 0; + padding: 0; + border: none; + margin-top: 1rem; + + &:first-of-type { + margin-top: 1rem; + } +`; + +const Legend = styled.legend` + font-size: 1.5rem; + padding: 0.5rem 0; + font-weight: bold; +`; + const DO_YOU_WANT_TO_CONTINUE = "Some settings in JSON can be lost when migrating. " + "Do you want to continue?"; @@ -40,47 +63,47 @@ class SettingsComponent extends React.Component<Props> { renderFormFields(form: FormSettings) { return ( <div> - <fieldset> - <legend>Keybindings</legend> + <Fieldset> + <Legend>Keybindings</Legend> <KeymapsForm value={form.keymaps} onChange={this.bindKeymapsForm.bind(this)} onBlur={this.save.bind(this)} /> - </fieldset> - <fieldset> - <legend>Search Engines</legend> + </Fieldset> + <Fieldset> + <Legend>Search Engines</Legend> <SearchForm value={form.search} onChange={this.bindSearchForm.bind(this)} onBlur={this.save.bind(this)} /> - </fieldset> - <fieldset> - <legend>Blacklist</legend> + </Fieldset> + <Fieldset> + <Legend>Blacklist</Legend> <BlacklistForm value={form.blacklist} onChange={this.bindBlacklistForm.bind(this)} onBlur={this.save.bind(this)} /> - </fieldset> - <fieldset> - <legend>Partial blacklist</legend> + </Fieldset> + <Fieldset> + <Legend>Partial blacklist</Legend> <PartialBlacklistForm value={form.blacklist} onChange={this.bindBlacklistForm.bind(this)} onBlur={this.save.bind(this)} /> - </fieldset> - <fieldset> - <legend>Properties</legend> + </Fieldset> + <Fieldset> + <Legend>Properties</Legend> <PropertiesForm types={Properties.types()} value={form.properties.toJSON()} onChange={this.bindPropertiesForm.bind(this)} onBlur={this.save.bind(this)} /> - </fieldset> + </Fieldset> </div> ); } @@ -88,8 +111,7 @@ class SettingsComponent extends React.Component<Props> { renderJsonFields(json: JSONTextSettings, error: string) { return ( <div> - <Input - type="textarea" + <TextArea name="json" label="Plain JSON" spellCheck={false} @@ -111,32 +133,28 @@ class SettingsComponent extends React.Component<Props> { fields = this.renderJsonFields(this.props.json!, this.props.error); } return ( - <div> + <Container> <h1>Configure Vim-Vixen</h1> - <form className="vimvixen-settings-form"> - <Input - type="radio" - id="setting-source-form" - name="source" - label="Use form" - checked={this.props.source === "form"} - value="form" - onValueChange={this.bindSource.bind(this)} - disabled={disabled} - /> + <Radio + id="setting-source-form" + name="source" + label="Use form" + checked={this.props.source === "form"} + value="form" + onValueChange={this.bindSource.bind(this)} + disabled={disabled} + /> - <Input - type="radio" - name="source" - label="Use plain JSON" - checked={this.props.source === "json"} - value="json" - onValueChange={this.bindSource.bind(this)} - disabled={disabled} - /> - {fields} - </form> - </div> + <Radio + name="source" + label="Use plain JSON" + checked={this.props.source === "json"} + value="json" + onValueChange={this.bindSource.bind(this)} + disabled={disabled} + /> + {fields} + </Container> ); } diff --git a/src/settings/components/site.scss b/src/settings/components/site.scss deleted file mode 100644 index c0c4f9e..0000000 --- a/src/settings/components/site.scss +++ /dev/null @@ -1,27 +0,0 @@ -.vimvixen-settings-form { - padding: 2px; - - textarea[name=json] { - font-family: monospace; - width: 100%; - min-height: 64ex; - resize: vertical; - } - - fieldset { - margin: 0; - padding: 0; - border: none; - margin-top: 1rem; - - fieldset:first-of-type { - margin-top: 1rem; - } - - legend { - font-size: 1.5rem; - padding: .5rem 0; - font-weight: bold; - } - } -} diff --git a/src/settings/components/ui/AddButton.scss b/src/settings/components/ui/AddButton.scss deleted file mode 100644 index beb5688..0000000 --- a/src/settings/components/ui/AddButton.scss +++ /dev/null @@ -1,13 +0,0 @@ -.ui-add-button { - border: none; - padding: 4; - display: inline; - background: none; - font-weight: bold; - color: green; - cursor: pointer; - - &:hover { - color: darkgreen; - } -} diff --git a/src/settings/components/ui/AddButton.tsx b/src/settings/components/ui/AddButton.tsx index c15a732..8cf4300 100644 --- a/src/settings/components/ui/AddButton.tsx +++ b/src/settings/components/ui/AddButton.tsx @@ -1,19 +1,24 @@ -import "./AddButton.scss"; import React from "react"; +import styled from "styled-components"; -type Props = React.AllHTMLAttributes<HTMLInputElement>; +const Button = styled.input` + border: none; + padding: 4; + display: inline; + background: none; + font-weight: bold; + color: green; + cursor: pointer; -class AddButton extends React.Component<Props> { - render() { - return ( - <input - className="ui-add-button" - type="button" - value="✚" - {...this.props} - /> - ); + &:hover { + color: darkgreen; } -} +`; + +type Props = React.InputHTMLAttributes<HTMLInputElement>; + +const AddButton: React.FC<Props> = (props) => ( + <Button type="button" value="✚" {...props} /> +); export default AddButton; diff --git a/src/settings/components/ui/DeleteButton.scss b/src/settings/components/ui/DeleteButton.scss deleted file mode 100644 index 5932a72..0000000 --- a/src/settings/components/ui/DeleteButton.scss +++ /dev/null @@ -1,13 +0,0 @@ - -.ui-delete-button { - border: none; - padding: 4; - display: inline; - background: none; - color: red; - cursor: pointer; - - &:hover { - color: darkred; - } -} diff --git a/src/settings/components/ui/DeleteButton.tsx b/src/settings/components/ui/DeleteButton.tsx index df8976e..ce0183b 100644 --- a/src/settings/components/ui/DeleteButton.tsx +++ b/src/settings/components/ui/DeleteButton.tsx @@ -1,19 +1,23 @@ -import "./DeleteButton.scss"; import React from "react"; +import styled from "styled-components"; -type Props = React.AllHTMLAttributes<HTMLInputElement>; +const Button = styled.input` + border: none; + padding: 4; + display: inline; + background: none; + color: red; + cursor: pointer; -class DeleteButton extends React.Component<Props> { - render() { - return ( - <input - className="ui-delete-button" - type="button" - value="✖" - {...this.props} - /> - ); + &:hover { + color: darkred; } -} +`; + +type Props = React.InputHTMLAttributes<HTMLInputElement>; + +const DeleteButton: React.FC<Props> = (props) => ( + <Button type="button" value="✖" {...props} /> +); export default DeleteButton; diff --git a/src/settings/components/ui/Input.scss b/src/settings/components/ui/Input.scss deleted file mode 100644 index ad4daf8..0000000 --- a/src/settings/components/ui/Input.scss +++ /dev/null @@ -1,29 +0,0 @@ -.settings-ui-input { - page-break-inside: avoid; - - * { - page-break-inside: avoid; - } - - label { - font-weight: bold; - min-width: 14rem; - display: inline-block; - } - - input[type='text'] { - padding: 4px; - width: 8rem; - } - - input.input-crror, - textarea.input-error { - box-shadow: 0 0 2px red; - } - - &-error { - font-weight: bold; - color: red; - min-height: 1.5em; - } -} diff --git a/src/settings/components/ui/Input.tsx b/src/settings/components/ui/Input.tsx deleted file mode 100644 index 0e24277..0000000 --- a/src/settings/components/ui/Input.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from "react"; -import "./Input.scss"; - -interface Props extends React.AllHTMLAttributes<HTMLElement> { - name: string; - type: string; - error?: string; - label: string; - value: string; - onValueChange?: (name: string, value: string) => void; - onBlur?: (e: React.FocusEvent<Element>) => void; -} - -class Input extends React.Component<Props> { - renderText(props: Props) { - const inputClassName = props.error ? "input-error" : ""; - const pp = { ...props }; - delete pp.onValueChange; - return ( - <div className="settings-ui-input"> - <label htmlFor={props.id}>{props.label}</label> - <input - className={inputClassName} - onChange={this.bindOnChange.bind(this)} - {...pp} - /> - </div> - ); - } - - renderRadio(props: Props) { - const inputClassName = props.error ? "input-error" : ""; - const pp = { ...props }; - delete pp.onValueChange; - return ( - <div className="settings-ui-input"> - <label> - <input - className={inputClassName} - onChange={this.bindOnChange.bind(this)} - {...pp} - /> - {props.label} - </label> - </div> - ); - } - - renderTextArea(props: Props) { - const inputClassName = props.error ? "input-error" : ""; - const pp = { ...props }; - delete pp.onValueChange; - return ( - <div className="settings-ui-input"> - <label htmlFor={props.id}>{props.label}</label> - <textarea - className={inputClassName} - onChange={this.bindOnChange.bind(this)} - {...pp} - /> - <p className="settings-ui-input-error">{this.props.error}</p> - </div> - ); - } - - render() { - const { type } = this.props; - - switch (this.props.type) { - case "text": - return this.renderText(this.props); - case "radio": - return this.renderRadio(this.props); - case "textarea": - return this.renderTextArea(this.props); - default: - console.warn(`Unsupported input type ${type}`); - } - return null; - } - - bindOnChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) { - if (this.props.onValueChange) { - this.props.onValueChange(e.target.name, e.target.value); - } - } -} - -export default Input; diff --git a/src/settings/components/ui/Radio.tsx b/src/settings/components/ui/Radio.tsx new file mode 100644 index 0000000..c0d4dd9 --- /dev/null +++ b/src/settings/components/ui/Radio.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + font-family: system-ui; +`; + +interface Props extends React.InputHTMLAttributes<HTMLInputElement> { + label: string; + onValueChange?: (name: string, value: string) => void; +} + +const Radio: React.FC<Props> = (props) => { + const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + if (props.onValueChange) { + props.onValueChange(e.target.name, e.target.value); + } + }; + + const pp = { ...props }; + delete pp.onValueChange; + + return ( + <Container> + <label htmlFor={props.id}> + <input type="radio" onChange={onChange} {...pp} /> + {props.label} + </label> + </Container> + ); +}; + +export default Radio; diff --git a/src/settings/components/ui/Text.tsx b/src/settings/components/ui/Text.tsx new file mode 100644 index 0000000..700b08a --- /dev/null +++ b/src/settings/components/ui/Text.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + page-break-inside: avoid; +`; + +const Input = styled.input<{ hasError: boolean }>` + padding: 4px; + width: 8rem; + box-shadow: ${({ hasError }) => (hasError ? "0 0 2px red" : "none")}; +`; + +const Label = styled.label` + font-weight: bold; + min-width: 14rem; + display: inline-block; +`; + +interface Props extends React.HTMLAttributes<HTMLElement> { + name: string; + error?: string; + label: string; + value: string; + onValueChange?: (name: string, value: string) => void; +} + +const Text: React.FC<Props> = (props) => { + const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + if (props.onValueChange) { + props.onValueChange(e.target.name, e.target.value); + } + }; + + const pp = { ...props }; + delete pp.onValueChange; + + return ( + <Container> + <Label> + {props.label} + <br /> + <Input + type="text" + hasError={props.error !== undefined} + onChange={onChange} + {...pp} + /> + </Label> + </Container> + ); +}; + +export default Text; diff --git a/src/settings/components/ui/TextArea.tsx b/src/settings/components/ui/TextArea.tsx new file mode 100644 index 0000000..9dcd5be --- /dev/null +++ b/src/settings/components/ui/TextArea.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + page-break-inside: avoid; +`; + +const Label = styled.label` + font-weight: bold; + min-width: 14rem; + display: inline-block; +`; + +const ErrorableTextArea = styled.textarea<{ hasError: boolean }>` + box-shadow: ${({ hasError }) => (hasError ? "0 0 2px red" : "none")}; + font-family: monospace; + font-family: monospace; + width: 100%; + min-height: 64ex; + resize: vertical; +`; + +const ErrorMessage = styled.p` + font-weight: bold; + color: red; + min-height: 1.5em; +`; + +interface Props extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { + error?: string; + label: string; + onValueChange?: (name: string, value: string) => void; +} + +const TextArea: React.FC<Props> = (props) => { + const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + if (props.onValueChange) { + props.onValueChange(e.target.name, e.target.value); + } + }; + + const hasError = typeof props.error !== "undefined" && props.error !== ""; + const pp = { ...props }; + delete pp.onValueChange; + return ( + <Container> + <Label htmlFor={props.id}>{props.label}</Label> + <ErrorableTextArea hasError={hasError} onChange={onChange} {...pp} /> + {hasError ? ( + <ErrorMessage role="alert">{props.error}</ErrorMessage> + ) : null} + </Container> + ); +}; + +export default TextArea; |