Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Types: Add types to <EditableList /> component #1886

Merged
merged 2 commits into from
Feb 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 73 additions & 30 deletions lib/editable-list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import SmallCrossOutlineIcon from '../icons/cross-outline-small';
import ReorderIcon from '../icons/reorder';

export class EditableList extends Component {
import * as S from '../state';
import * as T from '../types';

type OwnProps = {
className: string;
getItemKey: (item: T.TagEntity) => T.EntityId;
onRemove: (tag: T.TagEntity) => any;
onReorder: (tags: T.TagEntity[]) => any;
renderItem: (tag: T.TagEntity) => React.ReactNode;
};

type StateProps = {
editing: boolean;
items: T.TagEntity[] | null;
sortTagsAlpha: boolean;
};

type Props = OwnProps & StateProps;

export class EditableList extends Component<Props> {
static displayName = 'EditableList';

static propTypes = {
className: PropTypes.string,
editing: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired,
renderItem: PropTypes.func.isRequired,
sortTagsAlpha: PropTypes.bool.isRequired,
getItemKey: PropTypes.func,
onRemove: PropTypes.func,
onReorder: PropTypes.func,
};
reorderingElement: HTMLElement | null = null;
reorderingClientY: number = 0;
reorderingOffsetY: number = 0;
reorderingTranslateY: number = 0;

static defaultProps = {
getItemKey: item => item.id,
getItemKey: (item: T.TagEntity) => item.id,
};

state = {
Expand All @@ -32,17 +44,17 @@ export class EditableList extends Component {
componentWillMount() {
this.setState({
items: this.props.items,
reorderedItems: this.props.items.slice(),
reorderedItems: this.props.items ? this.props.items.slice() : [],
});
}

componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps: Props) {
if (nextProps.items !== this.state.items) {
this.stopReordering();

this.setState({
items: nextProps.items,
reorderedItems: nextProps.items.slice(),
reorderedItems: nextProps.items ? nextProps.items.slice() : [],
});
}
}
Expand Down Expand Up @@ -95,7 +107,7 @@ export class EditableList extends Component {
{onRemove && (
<span
className="editable-list-trash"
tabIndex={editing ? '0' : '-1'}
tabIndex={editing ? 0 : -1}
onClick={onRemove.bind(null, item)}
>
<SmallCrossOutlineIcon />
Expand All @@ -110,7 +122,7 @@ export class EditableList extends Component {
{onReorder && !sortTagsAlpha && (
<span
className="editable-list-reorder"
tabIndex={editing ? '0' : '-1'}
tabIndex={editing ? 0 : -1}
onDragStart={e => e.preventDefault()}
onMouseDown={this.onReorderStart.bind(this, itemId)}
onTouchStart={this.onReorderStart.bind(this, itemId)}
Expand All @@ -126,7 +138,10 @@ export class EditableList extends Component {
);
}

setReorderingElementRef = (reorderingId, element) => {
setReorderingElementRef = (
reorderingId: T.EntityId,
element: HTMLElement
) => {
if (reorderingId !== this.state.reorderingId) {
return;
}
Expand Down Expand Up @@ -177,12 +192,12 @@ export class EditableList extends Component {
this.reorderingElement = null;
};

stopReordering = resetOrdering => {
stopReordering = (resetOrdering = false) => {
this.unsetReorderingElement();

if (resetOrdering) {
this.setState({
reorderedItems: this.props.items.slice(),
reorderedItems: this.props.items ? this.props.items.slice() : [],
reorderingId: null,
});
} else {
Expand All @@ -192,10 +207,15 @@ export class EditableList extends Component {
}
};

reorderItemById = (reorderingId, offset, targetReorderingId) => {
reorderItemById = (
reorderingId: T.EntityId | null,
offset: number,
targetReorderingId: T.EntityId | null = null
) => {
const { getItemKey } = this.props;
const { reorderedItems } = this.state;
let reorderingIndex, targetIndex;
let reorderingIndex = null;
let targetIndex = null;

if (targetReorderingId === null) {
targetReorderingId = reorderingId;
Expand All @@ -215,7 +235,10 @@ export class EditableList extends Component {
return this.reorderItemByIndex(reorderingIndex, targetIndex + offset);
};

reorderItemByIndex = (reorderingIndex, targetIndex) => {
reorderItemByIndex = (
reorderingIndex: number | null,
targetIndex: number | null
) => {
if (reorderingIndex === targetIndex) {
return;
}
Expand All @@ -235,7 +258,13 @@ export class EditableList extends Component {
this.setState({ reorderedItems });
};

onReorderStart = (reorderingId, event) => {
onReorderStart = (
reorderingId: T.EntityId,
event: T.XOR<
React.MouseEvent<HTMLSpanElement>,
React.TouchEvent<HTMLSpanElement>
>
) => {
event.preventDefault();
event.currentTarget.focus();

Expand All @@ -252,7 +281,13 @@ export class EditableList extends Component {
this.bindReorderingListeners();
};

onReorderMove = (targetReorderingId, event) => {
onReorderMove = (
targetReorderingId: T.EntityId,
event: T.XOR<
React.MouseEvent<HTMLLIElement>,
React.TouchEvent<HTMLLIElement>
>
) => {
if (!this.reorderingElement || !this.props.editing) {
return;
}
Expand All @@ -276,15 +311,18 @@ export class EditableList extends Component {
this.positionReorderingElement();
};

onReorderEnd = event => {
onReorderEnd = (event: MouseEvent | TouchEvent) => {
event.preventDefault();
this.stopReordering();
this.props.onReorder(this.state.reorderedItems);
};

onReorderCancel = () => this.stopReordering(true);

onReorderKeyDown = (reorderingId, event) => {
onReorderKeyDown = (
reorderingId: T.EntityId,
event: React.KeyboardEvent<HTMLSpanElement>
) => {
if (event.key === 'ArrowUp') {
event.preventDefault();
this.reorderItemById(reorderingId, -1);
Expand All @@ -294,7 +332,7 @@ export class EditableList extends Component {
}
};

onKeyDown = event => {
onKeyDown = (event: KeyboardEvent) => {
const keyCode = event.keyCode || event.which;

if (keyCode === 27) {
Expand All @@ -304,7 +342,12 @@ export class EditableList extends Component {
};
}

const mapStateToProps = ({ settings: { sortTagsAlpha } }) => ({
const mapStateToProps: S.MapState<StateProps> = ({
appState: { editingTags, tags },
settings: { sortTagsAlpha },
}) => ({
editing: editingTags,
items: tags,
sortTagsAlpha,
});

Expand Down
2 changes: 0 additions & 2 deletions lib/tag-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ export class TagList extends Component {
</div>
<EditableList
className="tag-list-items"
items={tags}
editing={editingTags}
renderItem={this.renderItem}
onRemove={this.onTrashTag}
onReorder={this.onReorderTags}
Expand Down
13 changes: 13 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,16 @@ export type ListDisplayMode = 'expanded' | 'comfy' | 'condensed';
export type SortType = 'alphabetical' | 'creationDate' | 'modificationDate';
export type Theme = 'system' | 'light' | 'dark';
export type TranslatableString = string;

///////////////////////////////////////
// Language and Platform
///////////////////////////////////////

// Returns a type with the properties in T not also present in U
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like these need some explanation as to what they are.

Copy link
Member Author

@dmsnell dmsnell Feb 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can do and will add. I feel like XOR should be in the language. it's here because the | is only really useful when we have a property on which to discriminate alternative types.

type Colors = { name: 'red', value: 0xff000 } | { name: 'green', value: 0x00ff00 };

This fails if our types our different

type Size = { radius: number } | { width: number; height: number };

It should be able to do this but it can't. When we don't have that property we have to rely on more advanced typing. In this case it's a mapped type called XOR which does exactly what | seems like it should do. Without is part of it and gets the difference of keys between them. Here I've used XOR because MouseEvent and TouchEvent don't have the discriminating property.

type Size = XOR<{radius: number}, {width: number; height: number}>

We can keep adding to the types by nesting, unlike the convenient chaining of |

type Size = XOR<{radius: number}, XOR<{width: number; height: number}, {kg: number}>>


// Either type T or type U, a powered-up union/sum type
// useful when missing a discriminating property
export type XOR<T, U> = T | U extends object
? (Without<T, U> & U) | (Without<U, T> & T)
: T | U;