WIP <Compose> Refactor; <ActionsModal>; dropdowns

signup-info-prompt
kibigo! 2017-12-29 16:32:13 -08:00
parent 083170bec7
commit b4a3792201
7 changed files with 534 additions and 479 deletions

View File

@ -133,8 +133,12 @@ export default class Dropdown extends React.PureComponent {
this.props.onModalOpen({
status,
actions: items,
onClick: this.handleItemClick,
actions: items.map(
(item, i) => ({
...item,
name: `${item.text}-${i}`,
onClick: this.handleItemClick.bind(i),
}),
});
return;
@ -162,8 +166,7 @@ export default class Dropdown extends React.PureComponent {
}
}
handleItemClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
handleItemClick = (i, e) => {
const { action, to } = this.props.items[i];
this.handleClose();

View File

@ -0,0 +1,97 @@
// Inspired by <CommonLink> from Mastodon GO!
// ~ 😘 kibi!
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
// Utils.
import { assignHandlers } from 'flavours/glitch/util/react_helpers';
// Handlers.
const handlers = {
// We don't handle clicks that are made with modifiers, since these
// often have special browser meanings (eg, "open in new tab").
click (e) {
const { onClick } = this.props;
if (!onClick || e.button || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
return;
}
onClick(e);
e.preventDefault(); // Prevents following of the link
},
};
// The component.
export default class Link extends React.PureComponent {
// Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
}
// Rendering.
render () {
const { click } = this.handlers;
const {
children,
className,
href,
onClick,
role,
title,
...rest
} = this.props;
const computedClass = classNames('link', className, role);
// We assume that our `onClick` is a routing function and give it
// the qualities of a link even if no `href` is provided. However,
// if we have neither an `onClick` or an `href`, our link is
// purely presentational.
const conditionalProps = {};
if (href) {
conditionalProps.href = href;
conditionalProps.onClick = click;
} else if (onClick) {
conditionalProps.onClick = click;
conditionalProps.role = 'link';
conditionalProps.tabIndex = 0;
} else {
conditionalProps.role = 'presentation';
}
// If we were provided a `role` it overwrites any that we may have
// set above. This can be used for "links" which are actually
// buttons.
if (role) {
conditionalProps.role = role;
}
// Rendering. We set `rel='noopener'` for user privacy, and our
// `target` as `'_blank'`.
return (
<a
className={computedClass}
{...conditionalProps}
rel='noopener'
target='_blank'
title={title}
{...rest}
>{children}</a>
);
}
}
// Props.
Link.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
href: PropTypes.string, // The link destination
onClick: PropTypes.func, // A function to call instead of opening the link
role: PropTypes.string, // An ARIA role for the link
title: PropTypes.string, // A title for the link
};

View File

@ -80,11 +80,16 @@ const handlers = {
}) => ({
...rest,
active: value && name === value,
name,
onClick (e) {
e.preventDefault(); // Prevents focus from changing
onModalClose();
onChange(name);
},
onPassiveClick (e) {
e.preventDefault(); // Prevents focus from changing
onChange(name);
},
})
),
});
@ -191,7 +196,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
>
{({ opacity, scaleX, scaleY }) => (
<div
className='dropdown'
className='composer--options--dropdown__dropdown'
ref={this.setRef}
style={{
opacity: opacity,

View File

@ -91,6 +91,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent {
case !!icon:
return (
<Icon
className='icon'
fullwidth
icon={icon}
/>
@ -100,11 +101,11 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent {
}
}()}
{meta ? (
<div>
<div className='content'>
<strong>{text}</strong>
{meta}
</div>
) : <div>{text}</div>}
) : <div className='content'>{text}</div>}
</div>
);
}

View File

@ -6,15 +6,26 @@ import StatusContent from 'flavours/glitch/components/status_content';
import Avatar from 'flavours/glitch/components/avatar';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import DisplayName from 'flavours/glitch/components/display_name';
import IconButton from 'flavours/glitch/components/icon_button';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import Link from 'flavours/glitch/components/link';
import Toggle from 'react-toggle';
export default class ActionsModal extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
actions: PropTypes.array,
actions: PropTypes.arrayOf(PropTypes.shape({
active: PropTypes.bool,
href: PropTypes.string,
icon: PropTypes.string,
meta: PropTypes.node,
name: PropTypes.string,
on: PropTypes.bool,
onClick: PropTypes.func,
onPassiveClick: PropTypes.func,
text: PropTypes.node,
})),
};
renderAction = (action, i) => {
@ -22,17 +33,57 @@ export default class ActionsModal extends ImmutablePureComponent {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { icon = null, text, meta = null, active = false, href = '#' } = action;
const {
active,
href,
icon,
meta,
name,
on,
onClick,
onPassiveClick,
text,
} = action;
return (
<li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' />}
<li key={name || i}>
<Link
className={classNames('link', { active })}
href={href}
onClick={onClick}
role={onClick ? 'button' : null}
>
{function () {
// We render a `<Toggle>` if we were provided an `on`
// property, and otherwise show an `<Icon>` if available.
switch (true) {
case on !== null && typeof on !== 'undefined':
return (
<Toggle
checked={on}
onChange={onPassiveClick || onClick}
/>
);
case !!icon:
return (
<Icon
className='icon'
fullwidth
icon={icon}
/>
);
default:
return null;
}
}()}
{meta ? (
<div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
<div>{meta}</div>
<strong>{text}</strong>
{meta}
</div>
</a>
) : <div>{text}</div>}
</Link>
</li>
);
}

View File

@ -1,7 +1,6 @@
.composer {
padding: 10px;
.composer { padding: 10px }
.composer--spoiler {
.composer--spoiler {
display: block;
box-sizing: border-box;
margin: 0;
@ -18,9 +17,9 @@
&:focus { outline: 0 }
@include single-column('screen and (max-width: 630px)') { font-size: 16px }
}
}
.composer--warning {
.composer--warning {
color: darken($ui-secondary-color, 65%);
margin-bottom: 15px;
background: $ui-primary-color;
@ -39,9 +38,9 @@
&:focus,
&:hover { text-decoration: none }
}
}
}
.composer--reply {
.composer--reply {
margin: 0 0 -2px;
border-radius: 4px 4px 0 0;
padding: 10px;
@ -114,9 +113,9 @@
}
}
}
}
}
.composer--textarea {
.composer--textarea {
background: $simple-background-color;
position: relative;
@ -146,8 +145,9 @@
resize: vertical;
}
}
}
.composer--textarea--suggestions {
.composer--textarea--suggestions {
display: block;
position: absolute;
box-sizing: border-box;
@ -162,8 +162,9 @@
z-index: 99;
&[hidden] { display: none }
}
.composer--textarea--suggestions--item {
.composer--textarea--suggestions--item {
display: flex;
flex-direction: row;
align-items: center;
@ -188,11 +189,9 @@
height: 16px;
}
}
}
}
}
}
.composer--upload_form {
.composer--upload_form {
display: flex;
flex-direction: row;
flex-wrap: wrap;
@ -202,8 +201,9 @@
font-size: 14px;
font-family: inherit;
overflow: hidden;
}
.composer--upload_form--item {
.composer--upload_form--item {
flex: 1 1 0;
margin: 5px;
min-width: 40%;
@ -252,10 +252,9 @@
input { opacity: 1 }
}
}
}
}
}
.composer--options {
.composer--options {
padding: 10px;
background: darken($simple-background-color, 8%);
box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05);
@ -277,9 +276,66 @@
padding: 0;
background: transparent;
}
}
.composer--options--dropdown {
& > .value { transition: none }
&.active {
& > .value {
border-radius: 4px 4px 0 0;
box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
color: $primary-text-color;
background: $ui-highlight-color;
}
}
}
.composer--options--dropdown__dropdown {
position: absolute;
margin-left: 40px;
border-radius: 4px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
background: $simple-background-color;
overflow: hidden;
transform-origin: 50% 0;
}
.composer--options--dropdown--item {
color: $ui-base-color;
padding: 10px;
cursor: pointer;
display: flex;
& > .content {
flex: 1 1 auto;
color: darken($ui-primary-color, 24%);
&:not(:first-child) { margin-left: 10px }
strong {
display: block;
color: $ui-base-color;
font-weight: 500;
}
}
.composer--publisher {
&:hover,
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
& > .content {
color: $primary-text-color;
strong { color: $primary-text-color }
}
}
&.active:hover { background: lighten($ui-highlight-color, 4%) }
}
.composer--publisher {
padding-top: 10px;
text-align: right;
white-space: nowrap;
@ -311,5 +367,4 @@
&.over {
& > .count { color: $warning-red }
}
}
}

View File

@ -2784,156 +2784,6 @@
filter: none;
}
.privacy-dropdown__dropdown {
position: absolute;
background: $simple-background-color;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
border-radius: 4px;
margin-left: 40px;
overflow: hidden;
transform-origin: 50% 0;
}
.privacy-dropdown__option {
color: $ui-base-color;
padding: 10px;
cursor: pointer;
display: flex;
&:hover,
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
.privacy-dropdown__option__content {
color: $primary-text-color;
strong {
color: $primary-text-color;
}
}
}
&.active:hover {
background: lighten($ui-highlight-color, 4%);
}
}
.privacy-dropdown__option__icon {
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.privacy-dropdown__option__content {
flex: 1 1 auto;
color: darken($ui-primary-color, 24%);
strong {
font-weight: 500;
display: block;
color: $ui-base-color;
}
}
.privacy-dropdown.active {
.privacy-dropdown__value {
background: $simple-background-color;
border-radius: 4px 4px 0 0;
box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
.icon-button {
transition: none;
}
&.active {
background: $ui-highlight-color;
.icon-button {
color: $primary-text-color;
}
}
}
.privacy-dropdown__dropdown {
display: block;
box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1);
}
}
.advanced-options-dropdown {
position: relative;
}
.advanced-options-dropdown__dropdown {
display: none;
position: absolute;
left: 0;
top: 27px;
width: 210px;
background: $simple-background-color;
border-radius: 0 4px 4px;
z-index: 2;
overflow: hidden;
}
.advanced-options-dropdown__option {
color: $ui-base-color;
padding: 10px;
cursor: pointer;
display: flex;
&:hover,
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
.advanced-options-dropdown__option__content {
color: $primary-text-color;
strong {
color: $primary-text-color;
}
}
}
&.active:hover {
background: lighten($ui-highlight-color, 4%);
}
}
.advanced-options-dropdown__option__toggle {
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.advanced-options-dropdown__option__content {
flex: 1 1 auto;
color: darken($ui-primary-color, 24%);
strong {
font-weight: 500;
display: block;
color: $ui-base-color;
}
}
.advanced-options-dropdown.open {
.advanced-options-dropdown__value {
background: $simple-background-color;
border-radius: 4px 4px 0 0;
box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
}
.advanced-options-dropdown__dropdown {
display: block;
box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1);
}
}
.modal-root {
transition: opacity 0.3s linear;
will-change: opacity;
@ -3488,7 +3338,7 @@
max-height: 80vh;
max-width: 80vw;
.actions-modal__item-label {
strong {
font-weight: 500;
}
@ -3501,31 +3351,24 @@
}
li:not(:empty) {
a {
& > .link {
color: $ui-base-color;
display: flex;
padding: 12px 16px;
font-size: 15px;
align-items: center;
text-decoration: none;
&,
button {
transition: none;
}
&.active,
&:hover,
&:active,
&:focus {
&,
button {
background: $ui-highlight-color;
color: $primary-text-color;
}
}
button:first-child {
& > .icon {
margin-right: 10px;
}
}