Files
outline/app/components/Input.tsx
Tom Moor fc8c20149f feat: Comments (#4911)
* Comment model

* Framework, model, policy, presenter, api endpoint etc

* Iteration, first pass of UI

* fixes, refactors

* Comment commands

* comment socket support

* typing indicators

* comment component, styling

* wip

* right sidebar resize

* fix: CMD+Enter submit

* Add usePersistedState
fix: Main page scrolling on comment highlight

* drafts

* Typing indicator

* refactor

* policies

* Click thread to highlight
Improve comment timestamps

* padding

* Comment menu v1

* Change comments to use editor

* Basic comment editing

* fix: Hide commenting button when disabled at team level

* Enable opening sidebar without mark

* Move selected comment to location state

* Add comment delete confirmation

* Add comment count to document meta

* fix: Comment sidebar togglable
Add copy link to comment

* stash

* Restore History changes

* Refactor right sidebar to allow for comment animation

* Update to new router best practices

* stash

* Various improvements

* stash

* Handle click outside

* Fix incorrect placeholder in input
fix: Input box appearing on other sessions erroneously

* stash

* fix: Don't leave orphaned child comments

* styling

* stash

* Enable comment toggling again

* Edit styling, merge conflicts

* fix: Cannot navigate from insights to comments

* Remove draft comment mark on click outside

* Fix: Empty comment sidebar, tsc

* Remove public toggle

* fix: All comments are recessed
fix: Comments should not be printed

* fix: Associated mark should be removed on comment delete

* Revert unused changes

* Empty state, basic RTL support

* Create dont toggle comment mark

* Make it feel more snappy

* Highlight active comment in text

* fix animation

* RTL support

* Add reply CTA

* Translations
2023-02-25 12:03:05 -08:00

238 lines
5.7 KiB
TypeScript

import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import { undraggableOnDesktop } from "~/styles";
const RealTextarea = styled.textarea<{ hasIcon?: boolean }>`
border: 0;
flex: 1;
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
outline: none;
background: none;
color: ${(props) => props.theme.text};
&:disabled,
&::placeholder {
color: ${(props) => props.theme.placeholder};
}
`;
const RealInput = styled.input<{ hasIcon?: boolean }>`
border: 0;
flex: 1;
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
outline: none;
background: none;
color: ${(props) => props.theme.text};
height: 30px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
${undraggableOnDesktop()}
&:disabled,
&::placeholder {
color: ${(props) => props.theme.placeholder};
}
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0px 1000px ${(props) => props.theme.background}
inset;
}
&::-webkit-search-cancel-button {
-webkit-appearance: none;
}
${breakpoint("mobile", "tablet")`
font-size: 16px;
`};
`;
const Wrapper = styled.div<{
flex?: boolean;
short?: boolean;
minHeight?: number;
minWidth?: number;
maxHeight?: number;
}>`
flex: ${(props) => (props.flex ? "1" : "0")};
width: ${(props) => (props.short ? "49%" : "auto")};
max-width: ${(props) => (props.short ? "350px" : "100%")};
min-width: ${({ minWidth }) => (minWidth ? `${minWidth}px` : "initial")};
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "initial")};
`;
const IconWrapper = styled.span`
position: relative;
left: 4px;
width: 24px;
height: 24px;
`;
export const Outline = styled(Flex)<{
margin?: string | number;
hasError?: boolean;
focused?: boolean;
}>`
flex: 1;
margin: ${(props) =>
props.margin !== undefined ? props.margin : "0 0 16px"};
color: inherit;
border-width: 1px;
border-style: solid;
border-color: ${(props) =>
props.hasError
? props.theme.danger
: props.focused
? props.theme.inputBorderFocused
: props.theme.inputBorder};
border-radius: 4px;
font-weight: normal;
align-items: center;
overflow: hidden;
background: ${(props) => props.theme.background};
/* Prevents an issue where input placeholder appears in a selected style when double clicking title bar */
user-select: none;
`;
export const LabelText = styled.div`
font-weight: 500;
padding-bottom: 4px;
display: inline-block;
`;
export type Props = React.InputHTMLAttributes<
HTMLInputElement | HTMLTextAreaElement
> & {
type?: "text" | "email" | "checkbox" | "search" | "textarea";
labelHidden?: boolean;
label?: string;
flex?: boolean;
short?: boolean;
margin?: string | number;
error?: string;
icon?: React.ReactNode;
/* Callback is triggered with the CMD+Enter keyboard combo */
onRequestSubmit?: (ev: React.KeyboardEvent<HTMLInputElement>) => unknown;
onFocus?: (ev: React.SyntheticEvent) => unknown;
onBlur?: (ev: React.SyntheticEvent) => unknown;
};
function Input(
props: Props,
ref: React.RefObject<HTMLInputElement | HTMLTextAreaElement>
) {
const [focused, setFocused] = React.useState(false);
const handleBlur = (ev: React.SyntheticEvent) => {
setFocused(false);
if (props.onBlur) {
props.onBlur(ev);
}
};
const handleFocus = (ev: React.SyntheticEvent) => {
setFocused(true);
if (props.onFocus) {
props.onFocus(ev);
}
};
const handleKeyDown = (
ev: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
if (ev.key === "Enter" && ev.metaKey) {
if (this.props.onRequestSubmit) {
this.props.onRequestSubmit(ev);
}
}
if (this.props.onKeyDown) {
this.props.onKeyDown(ev);
}
};
const {
type = "text",
icon,
label,
margin,
error,
className,
short,
flex,
labelHidden,
onFocus,
onBlur,
...rest
} = props;
const wrappedLabel = <LabelText>{label}</LabelText>;
return (
<Wrapper className={className} short={short} flex={flex}>
<label>
{label &&
(labelHidden ? (
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
) : (
wrappedLabel
))}
<Outline focused={focused} margin={margin}>
{icon && <IconWrapper>{icon}</IconWrapper>}
{type === "textarea" ? (
<RealTextarea
ref={ref as React.RefObject<HTMLTextAreaElement>}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
hasIcon={!!icon}
{...rest}
/>
) : (
<RealInput
ref={ref as React.RefObject<HTMLInputElement>}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
hasIcon={!!icon}
type={type}
{...rest}
/>
)}
</Outline>
</label>
{error && (
<TextWrapper>
<StyledText type="danger" size="xsmall">
{error}
</StyledText>
</TextWrapper>
)}
</Wrapper>
);
}
export const TextWrapper = styled.span`
min-height: 16px;
display: block;
margin-top: -16px;
`;
export const StyledText = styled(Text)`
margin-bottom: 0;
`;
export default React.forwardRef(Input);