Compare commits

...

9 Commits

21 changed files with 190 additions and 717 deletions

View File

@ -9,6 +9,14 @@
},
"plugins": ["compat"],
"rules": {
"no-mixed-operators": [
"error", { "groups": [
["&", "|", "^", "~", "<<", ">>", ">>>"],
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
["&&", "||"],
["in", "instanceof"]
] }
],
"compat/compat": 2,
"func-names": "off",
"global-require": "off",

View File

@ -48,6 +48,7 @@ const factory = (Chip, Input) => {
selectedPosition: PropTypes.oneOf(['above', 'below', 'none']),
source: PropTypes.any,
minWidth: PropTypes.number,
more: PropTypes.string,
suggestionMatch: PropTypes.oneOf(['disabled', 'start', 'anywhere', 'word', 'none']),
theme: PropTypes.shape({
active: PropTypes.string,
@ -373,6 +374,7 @@ const factory = (Chip, Input) => {
<ul style={{bottom}}
className={theme.suggestions}>
{this.renderSuggestions()}
{this.props.more ? <li className={theme.suggestion}>{this.props.more}</li> : null}
</ul>
</Transition>
</TransitionGroup>);
@ -381,17 +383,17 @@ const factory = (Chip, Input) => {
render() {
const {
placeholder, allowClear, className, clearTooltip, disabled,
error, label, value, selectedPosition, theme, multiple
error, label, value, selectedPosition, style, theme, multiple
} = this.props;
const inputProps = this.props.inputProps || {};
const outerClassName = classnames(theme.autocomplete, {
[theme.focus]: this.state.focus,
}, className);
const withClear = allowClear && (multiple
const withClear = allowClear && !disabled && (multiple
? value && Object.keys(value).length > 0
: value != null);
return (
<div data-react-toolbox="autocomplete" className={outerClassName}>
<div data-react-toolbox="autocomplete" className={outerClassName} style={style}>
{selectedPosition === 'above' ? this.renderSelected() : null}
{withClear ? <span
className={'material-icons '+theme.clear}

View File

@ -21,11 +21,11 @@
}
.withclear input {
text-indent: 28px !important;
text-indent: 28px;
}
.withclear label {
transition-property: top, left, font-size, color !important;
transition-property: top, left, font-size, color;
}
.inputFilled {
@ -33,7 +33,7 @@
}
.withclear input:not(:focus):not(.inputFilled) ~ label {
left: 28px !important;
left: 28px;
}
.values {

View File

@ -69,6 +69,7 @@ const factory = (Check) => {
className={className}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={style}
>
<input
{...others}
@ -85,7 +86,6 @@ const factory = (Check) => {
checked={checked}
disabled={disabled}
rippleClassName={theme.ripple}
style={style}
theme={theme}
/>
{label ? <span data-react-toolbox="label" className={theme.text}>{label}</span> : null}

View File

@ -20,6 +20,8 @@ const factory = (Overlay, Button) => {
[props.theme.active]: props.active,
}, props.className);
const style = props.style; // eslint-disable-line
return (
<Portal className={props.theme.wrapper}>
<Overlay
@ -33,7 +35,7 @@ const factory = (Overlay, Button) => {
theme={props.theme}
themeNamespace="overlay"
/>
<div data-react-toolbox="dialog" className={className}>
<div data-react-toolbox="dialog" className={className} style={style}>
<section role="body" className={props.theme.body}>
{props.title ? <h6 className={props.theme.title}>{props.title}</h6> : null}
{props.children}

View File

@ -1,132 +0,0 @@
import * as React from "react";
import ReactToolbox from "../index";
export interface DropdownTheme {
/**
* Added to the root element when the dropdown is active.
*/
active?: string;
/**
* Added to the root element when it's disabled.
*/
disabled?: string;
/**
* Root element class.
*/
dropdown?: string;
/**
* Used for the error element.
*/
error?: string;
/**
* Added to the inner wrapper if it's errored.
*/
errored?: string;
/**
* Used for the inner wrapper of the component.
*/
field?: string;
/**
* Used for the the label element.
*/
label?: string;
/**
* Used when dropdown has required attribute.
*/
required?: string;
/**
* Used to highlight the selected value.
*/
selected?: string;
/**
* Used as a wrapper for the given template value.
*/
templateValue?: string;
/**
* Added to the root element when it's opening up.
*/
up?: string;
/**
* Used for each value in the dropdown component.
*/
value?: string;
/**
* Used for the list of values.
*/
values?: string;
}
export interface DropdownProps extends ReactToolbox.Props {
/**
* If true the dropdown will preselect the first item if the supplied value matches none of the options' values.
* @default true
*/
allowBlank?: boolean;
/**
* If true, the dropdown will open up or down depending on the position in the screen.
* @default true
*/
auto?: boolean;
/**
* Set the component as disabled.
* @default false
*/
disabled?: boolean;
/**
* Give an error string to display under the field.
*/
error?: string;
/**
* The text string to use for the floating label element.
*/
label?: string;
/**
* Used for setting the label from source
*/
labelKey?: string;
/**
* Name for the input field.
*/
name?: string;
/**
* Callback function that is fired when the component is blurred.
*/
onBlur?: Function;
/**
* Callback function that is fired when the component's value changes.
*/
onChange?: Function;
/**
* Callback function that is fired when the component is focused.
*/
onFocus?: Function;
/**
* If true, the dropdown has a required attribute.
* @default false
*/
required?: boolean;
/**
* Array of data objects with the data to represent in the dropdown.
*/
source: any[];
/**
* Callback function that returns a JSX template to represent the element.
*/
template?: Function;
/**
* Classnames object defining the component style.
*/
theme?: DropdownTheme;
/**
* Default value using JSON data.
*/
value?: string | number;
/**
* Used for setting the value from source
*/
valueKey?: string;
}
export class Dropdown extends React.Component<DropdownProps, {}> { }
export default Dropdown;

View File

@ -1,247 +0,0 @@
/* eslint-disable */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import classnames from 'classnames';
import { themr } from 'react-css-themr';
import { DROPDOWN } from '../identifiers';
import InjectInput from '../input/Input';
import events from '../utils/events';
const factory = (Input) => {
class Dropdown extends Component {
static propTypes = {
allowBlank: PropTypes.bool,
allowClear: PropTypes.bool,
clearTooltip: PropTypes.string,
auto: PropTypes.bool,
className: PropTypes.string,
disabled: PropTypes.bool,
error: PropTypes.string,
label: PropTypes.string,
labelKey: PropTypes.string,
name: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onClick: PropTypes.func,
onFocus: PropTypes.func,
required: PropTypes.bool,
source: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
])).isRequired,
template: PropTypes.func,
theme: PropTypes.shape({
active: PropTypes.string,
disabled: PropTypes.string,
dropdown: PropTypes.string,
error: PropTypes.string,
errored: PropTypes.string,
field: PropTypes.string,
label: PropTypes.string,
required: PropTypes.string,
selected: PropTypes.string,
templateValue: PropTypes.string,
up: PropTypes.string,
value: PropTypes.string,
values: PropTypes.string,
}),
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
valueKey: PropTypes.string,
};
static defaultProps = {
auto: true,
className: '',
allowBlank: true,
allowClear: false,
clearTooltip: 'Clear',
disabled: false,
labelKey: 'label',
required: false,
valueKey: 'value',
};
state = {
active: false,
up: false,
};
componentWillUpdate(nextProps, nextState) {
if (!this.state.active && nextState.active) {
events.addEventsToDocument(this.getDocumentEvents());
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.active && !this.state.active) {
events.removeEventsFromDocument(this.getDocumentEvents());
}
}
componentWillUnmount() {
if (this.state.active) {
events.removeEventsFromDocument(this.getDocumentEvents());
}
}
getDocumentEvents = () => ({
click: this.handleDocumentClick,
touchend: this.handleDocumentClick,
});
getSelectedItem = () => {
for (const item of this.props.source) {
if (item[this.props.valueKey] === this.props.value) return item;
}
return !this.props.allowBlank
? this.props.source[0]
: undefined;
};
handleSelect = (item, event) => {
if (!this.props.disabled && this.props.onChange) {
if (this.props.name) event.target.name = this.props.name;
this.props.onChange(item, event);
this.close();
}
};
handleClick = (event) => {
this.open(event);
events.pauseEvent(event);
if (this.props.onClick) this.props.onClick(event);
};
handleDocumentClick = (event) => {
if (this.state.active && !events.targetIsDescendant(event, ReactDOM.findDOMNode(this))) {
this.setState({ active: false });
}
};
close = () => {
if (this.state.active) {
this.setState({ active: false });
if (this.props.onBlur) this.props.onBlur(event);
}
}
open = (event) => {
if (this.state.active) return;
const client = event.target.getBoundingClientRect();
const screenHeight = window.innerHeight || document.documentElement.offsetHeight;
const up = this.props.auto ? client.top > ((screenHeight / 2) + client.height) : false;
this.setState({ active: true, up });
if (this.props.onFocus) this.props.onFocus(event);
};
handleFocus = (event) => {
event.stopPropagation();
if (!this.props.disabled) this.open(event);
};
handleBlur = (event) => {
event.stopPropagation();
if (this.state.active) this.close();
}
renderTemplateValue(selected) {
const { theme } = this.props;
const className = classnames(theme.field, {
[theme.errored]: this.props.error,
[theme.disabled]: this.props.disabled,
[theme.required]: this.props.required,
});
return (
<div className={className} onClick={this.handleClick}>
<div className={`${theme.templateValue} ${theme.value}`}>
{this.props.template(selected)}
</div>
{this.props.label
? (
<label className={theme.label}>
{this.props.label}
{this.props.required ? <span className={theme.required}> * </span> : null}
</label>
) : null}
{this.props.error ? <span className={theme.error}>{this.props.error}</span> : null}
</div>
);
}
renderValue = (item, idx) => {
const { labelKey, theme, valueKey } = this.props;
const className = classnames({
[theme.selected]: item[valueKey] === this.props.value,
[theme.disabled]: item.disabled,
});
return (
<li
key={idx}
className={className}
onMouseDown={!item.disabled && this.handleSelect.bind(this, item[valueKey])}
>
{this.props.template ? this.props.template(item) : item[labelKey]}
</li>
);
};
render() {
const {
allowBlank, allowClear, clearTooltip, auto, labelKey, required, onChange, onFocus, onBlur, // eslint-disable-line no-unused-vars
source, template, theme, valueKey, ...others
} = this.props;
const selected = this.getSelectedItem();
const className = classnames(theme.dropdown, {
[theme.up]: this.state.up,
[theme.active]: this.state.active,
[theme.disabled]: this.props.disabled,
[theme.required]: this.props.required,
[theme.withclear]: allowClear && selected,
}, this.props.className);
return (
<div
className={className}
style={{outline: 'none'}}
data-react-toolbox="dropdown"
tabIndex="-1"
>
{allowClear && selected ? <span
className={'material-icons '+theme.clear}
title={clearTooltip}
onClick={(e) => this.props.onChange(null, e)}>clear</span> : null}
<Input
{...others}
tabIndex="0"
className={theme.value}
onClick={this.handleClick}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
required={this.props.required}
readOnly
type={template && selected ? 'hidden' : null}
theme={theme}
themeNamespace="input"
value={selected && selected[labelKey] ? selected[labelKey] : ''}
/>
{template && selected ? this.renderTemplateValue(selected) : null}
<ul className={theme.values} style={this.state.up ? {bottom: '100%'} : {top: '100%'}}>
{source.map(this.renderValue)}
</ul>
</div>
);
}
}
return Dropdown;
};
const Dropdown = factory(InjectInput);
export default themr(DROPDOWN)(Dropdown);
export { factory as dropdownFactory };
export { Dropdown };

View File

@ -1,10 +0,0 @@
:root {
--dropdown-value-border-size: calc(var(--input-field-height) / 7);
--dropdown-color-white: var(--color-white);
--dropdown-color-primary: var(--color-primary);
--dropdown-color-primary-contrast: var(--color-primary-contrast);
--dropdown-color-disabled: color(var(--color-black) a(26%));
--dropdown-value-hover-background: var(--palette-grey-200);
--dropdown-overflow-max-height: 45vh;
--dropdown-value-border-radius: calc(0.2 * var(--unit));
}

View File

@ -1,5 +0,0 @@
import { Dropdown } from './Dropdown';
export { DropdownProps, DropdownTheme } from './Dropdown';
export { Dropdown }
export default Dropdown;

View File

@ -1,11 +1,26 @@
/* eslint-disable */
import { themr } from 'react-css-themr';
import { DROPDOWN } from '../identifiers';
import { dropdownFactory } from './Dropdown';
import { autocompleteFactory } from '../autocomplete';
import { Chip } from '../chip';
import { Input } from '../input';
import theme from './theme.css';
const Dropdown = dropdownFactory(Input);
const ThemedDropdown = themr(DROPDOWN, theme)(Dropdown);
import theme from '../autocomplete/theme.css';
import overrides from './overrides.css';
export default ThemedDropdown;
export { ThemedDropdown as Dropdown };
const overriddenTheme = { ...theme, inputInputElement: theme.inputInputElement+' '+overrides.inputInputElement };
const Autocomplete = autocompleteFactory(Chip, Input);
const ThemedAutocomplete = themr(AUTOCOMPLETE, overriddenTheme, { withRef: true })(Autocomplete);
export const Dropdown = (props) => <Autocomplete
readOnly
multiple={false}
keepFocusOnChange={false}
allowCreate={false}
showSelectedWhenNotInSource={false}
suggestionMatch="disabled"
{...props}
/>;
export default Dropdown;

View File

@ -0,0 +1,4 @@
.inputInputElement {
caret-color: transparent;
cursor: pointer;
}

View File

@ -1,73 +0,0 @@
# Dropdown
The Dropdown selects an option between multiple selections. The element displays the current state and a down arrow. When it is clicked, it displays the list of available options.
<!-- example -->
```jsx
import Dropdown from 'react-toolbox/lib/dropdown';
const countries = [
{ value: 'EN-gb', label: 'England' },
{ value: 'ES-es', label: 'Spain'},
{ value: 'TH-th', label: 'Thailand' },
{ value: 'EN-en', label: 'USA'}
];
class DropdownTest extends React.Component {
state = { value: 'ES-es' };
handleChange = (value) => {
this.setState({value: value});
};
render () {
return (
<Dropdown
auto
onChange={this.handleChange}
source={countries}
value={this.state.value}
/>
);
}
}
```
If you want to provide a theme via context, the component key is `RTDropdown`.
## Properties
| Name | Type | Default | Description |
|:-------------|:-----------|:--------|:------------|
| `allowBlank` | `Boolean` | `true` | If false the dropdown will preselect the first item if the supplied value matches none of the options' values.|
| `auto` | `Boolean` | `true` | If true, the dropdown will open up or down depending on the position in the screen.|
| `className` | `String` | `''` | Set the class to give custom styles to the dropdown.|
| `disabled` | `Boolean` | `false` | Set the component as disabled.|
| `error` | `String` | | Give an error string to display under the field.|
| `label` | `String` | | The text string to use for the floating label element.|
| `onBlur` | `Function` | | Callback function that is fired when the component is blurred.|
| `onChange` | `Function` | | Callback function that is fired when the component's value changes.|
| `onFocus` | `Function` | | Callback function that is fired when the component is focused.|
| `source` | `Array` | | Array of data objects with the data to represent in the dropdown.|
| `template` | `Function` | | Callback function that returns a JSX template to represent the element.|
| `value` | `String` | | Default value using JSON data.|
| `required` | `Boolean` | `false` | If true, the dropdown has a required attribute.|
## Theming
This component uses an `Input` under the covers. The theme object is passed down namespaced under `input` keyword. This means you can use the same theme classNames from `Input` component but namespaced with `input`. For example, to style the label you have to use `inputLabel` className.
| Name | Description|
|:----------------|:-----------|
| `active` | Added to the root element when the dropdown is active.|
| `disabled` | Added to the root element when it's disabled.|
| `dropdown` | Root element class.|
| `error` | Used for the error element.|
| `errored` | Added to the inner wrapper if it's errored.|
| `field` | Used for the inner wrapper of the component.|
| `label` | Used for the the label element.|
| `selected` | Used to highlight the selected value.|
| `templateValue` | Used as a wrapper for the given template value.|
| `up` | Added to the root element when it's opening up.|
| `value` | Used for each value in the dropdown component.|
| `values` | Used for the list of values.|

View File

@ -1,180 +0,0 @@
@import '../colors.css';
@import '../variables.css';
@import '../input/config.css';
@import './config.css';
.dropdown {
position: relative;
@apply --reset;
&:not(.active) {
& > .values {
max-height: 0;
visibility: hidden;
}
}
&.active {
& > .label,
& > .value {
opacity: 0.5;
}
& > .values {
box-shadow: var(--zdepth-shadow-1);
max-height: var(--dropdown-overflow-max-height);
visibility: visible;
}
}
&:not(.up) > .values {
bottom: auto;
top: 0;
}
&.up > .values {
bottom: 0;
top: auto;
}
&.disabled {
cursor: normal;
pointer-events: none;
}
&.withclear input {
text-indent: 28px !important;
}
}
.value {
& > input {
cursor: pointer;
}
&::after {
border-left: var(--dropdown-value-border-size) solid transparent;
border-right: var(--dropdown-value-border-size) solid transparent;
border-top: var(--dropdown-value-border-size) solid var(--input-text-bottom-border-color);
content: '';
height: 0;
pointer-events: none;
position: absolute;
right: var(--input-chevron-offset);
top: 50%;
transition: transform var(--animation-duration) var(--animation-curve-default);
width: 0;
}
}
.field {
cursor: pointer;
padding: var(--input-padding) 0;
position: relative;
&.errored {
padding-bottom: 0;
& > .label {
color: var(--input-text-error-color);
}
& > .templateValue {
border-bottom: 1px solid var(--input-text-error-color);
}
& > .label > .required {
color: var(--input-text-error-color);
}
}
&.disabled {
cursor: normal;
pointer-events: none;
& > .templateValue {
border-bottom-style: dotted;
opacity: 0.7;
}
}
}
.templateValue {
background-color: var(--input-text-background-color);
border-bottom: 1px solid var(--input-text-bottom-border-color);
color: var(--color-text);
min-height: var(--input-field-height);
padding: var(--input-field-padding) 0;
position: relative;
}
.label {
color: var(--input-text-label-color);
font-size: var(--input-label-font-size);
left: 0;
line-height: var(--input-field-font-size);
position: absolute;
top: var(--input-focus-label-top);
& .required {
color: var(--input-text-error-color);
}
}
.error {
color: var(--input-text-error-color);
font-size: var(--input-label-font-size);
line-height: var(--input-underline-height);
margin-bottom: calc(-1 * var(--input-underline-height));
}
.values {
background-color: var(--dropdown-color-white);
border-radius: var(--dropdown-value-border-radius);
list-style: none;
margin: 0;
overflow-y: auto;
padding: 0;
position: absolute;
transition-duration: var(--animation-duration);
transition-property: max-height, box-shadow;
transition-timing-function: var(--animation-curve-default);
width: 100%;
z-index: var(--z-index-high);
& > * {
cursor: pointer;
overflow: hidden;
padding: var(--unit);
position: relative;
&:hover:not(.disabled) {
background-color: var(--dropdown-value-hover-background);
}
&.selected {
color: var(--dropdown-color-primary);
}
&.disabled {
color: var(--dropdown-color-disabled);
cursor: not-allowed;
}
}
&::-webkit-scrollbar {
height: 0;
width: 0;
}
}
.clear {
cursor: pointer;
display: block;
left: -4px;
padding: 4px;
position: absolute;
top: 12px;
z-index: 10;
}

View File

@ -163,7 +163,7 @@ const factory = (FontIcon) => {
render() {
const { children, defaultValue, disabled, error, floating, hint, icon,
name, label: labelText, maxLength, multiline, required, role,
theme, type, value, onKeyPress, rows = 1, ...others } = this.props;
theme, type, value, onKeyPress, rows = 1, style, ...others } = this.props; // eslint-disable-line
const length = maxLength && value ? value.length : 0;
const labelClassName = classnames(theme.label, { [theme.fixed]: !floating });
@ -198,7 +198,7 @@ const factory = (FontIcon) => {
}
return (
<div data-react-toolbox="input" className={className}>
<div data-react-toolbox="input" className={className} style={style}>
{React.createElement(multiline ? 'textarea' : 'input', inputElementProps)}
{icon ? <FontIcon className={theme.icon} value={icon} /> : null}
<span className={theme.bar} />

View File

@ -66,6 +66,12 @@ const factory = (MenuItem) => {
: this.props.position;
this.setState({ position, width, height });
});
if (this.state.active) {
events.addEventsToDocument({
click: this.handleDocumentClick,
touchstart: this.handleDocumentClick,
});
}
}
componentWillReceiveProps(nextProps) {

View File

@ -58,12 +58,13 @@ const factory = (Thumb) => {
disabled,
onChange, // eslint-disable-line no-unused-vars
ripple,
style, // eslint-disable-line
theme,
...others
} = this.props;
const _className = classnames(theme[disabled ? 'disabled' : 'field'], className);
return (
<label data-react-toolbox="switch" className={_className}>
<label data-react-toolbox="switch" className={_className} style={style}>
<input
{...others}
checked={this.props.checked}

View File

@ -47,39 +47,44 @@
&.selected { background-color: var(--table-selection-color); }
}
/* common styles for all kinds of cells */
.table > thead > tr > th,
.table > tbody > tr > th,
.table > thead > tr > td,
.table > tbody > tr > td,
.rowCell,
.headCell {
border-bottom: var(--table-dividers);
height: var(--table-row-height);
padding: 0 var(--table-column-padding) 12px var(--table-column-padding);
text-align: left;
&:first-of-type { padding-left: 24px; }
&:last-of-type { padding-right: 24px; }
&.numeric { text-align: right; }
}
.table > thead > tr > td,
.table > tbody > tr > td,
.rowCell {
border-bottom: var(--table-dividers);
border-top: var(--table-dividers);
height: var(--table-row-height);
padding-top: var(--table-cell-top);
vertical-align: middle;
&.checkboxCell {
padding-right: 5px;
width: calc(1.8 * var(--unit));
}
}
/* styles for both header and normal body cells */
.table > tbody > tr > th,
.table > tbody > tr > td,
.rowCell {
border-top: var(--table-dividers);
padding-top: var(--table-cell-top);
vertical-align: middle;
&.checkboxCell {
& > * {
margin: 0;
}
}
}
/* styles for header cells both in body and header */
.table > thead > tr > th,
.table > tbody > tr > th,
.headCell {
@ -88,14 +93,17 @@
font-weight: 500;
height: var(--table-row-height);
line-height: calc(2.4 * var(--unit));
padding-bottom: 8px;
text-overflow: ellipsis;
}
/* styles for all cells in header */
.table > thead > tr > th,
.table > thead > tr > td,
.headCell {
padding-bottom: 8px;
vertical-align: bottom;
&.checkboxCell {
padding-right: 5px;
width: calc(1.8 * var(--unit));
& > * {
margin: 0 0 3px;
}

View File

@ -55,7 +55,9 @@ const tooltipFactory = (options = {}) => {
PropTypes.node,
]),
tooltipDelay: PropTypes.number,
tooltipForChildren: PropTypes.bool,
tooltipHideOnClick: PropTypes.bool,
tooltipOnFocus: PropTypes.bool,
tooltipPosition: PropTypes.oneOf(Object.keys(POSITION).map(key => POSITION[key])),
tooltipShowOnClick: PropTypes.bool,
};
@ -72,6 +74,9 @@ const tooltipFactory = (options = {}) => {
active: false,
position: this.props.tooltipPosition,
visible: false,
top: 0,
left: 0,
tooltip: '',
};
componentWillUnmount() {
@ -88,15 +93,44 @@ const tooltipFactory = (options = {}) => {
}
};
getPosition(element) {
setTooltipNode = (node) => {
this.tooltipNode = node;
if (node) {
const { width: vw, height: vh } = getViewport();
const { top, left, position } = this.state;
const width = this.tooltipNode.offsetWidth;
const height = this.tooltipNode.offsetHeight;
let x = -50;
let y = -50;
if (position === POSITION.TOP || position === POSITION.BOTTOM) {
y = position === POSITION.TOP ? -100 : 0;
if (left + width / 2 > vw) {
x = -Math.ceil(100 * (left + width - vw + 1) / width);
} else if (left - width / 2 < 0) {
x = Math.ceil(100 * (width - left) / width);
}
} else if (position === POSITION.LEFT || position === POSITION.RIGHT) {
x = position === POSITION.LEFT ? -100 : 0;
if (top + height / 2 > vh) {
y = -Math.ceil(100 * (top + height - vh + 1) / height);
} else if (top - height / 2 < 0) {
y = Math.ceil(100 * (height - top) / height);
}
}
this.setState({ transform: `scale(1) translateX(${x}%) translateY(${y}%)` });
this.timeout = setTimeout(() => {
this.setState({ active: true });
}, this.props.tooltipDelay);
}
}
getPosition(origin) {
const { tooltipPosition } = this.props;
if (tooltipPosition === POSITION.HORIZONTAL) {
const origin = element.getBoundingClientRect();
const { width: ww } = getViewport();
const toRight = origin.left < ((ww / 2) - (origin.width / 2));
return toRight ? POSITION.RIGHT : POSITION.LEFT;
} else if (tooltipPosition === POSITION.VERTICAL) {
const origin = element.getBoundingClientRect();
const { height: wh } = getViewport();
const toBottom = origin.top < ((wh / 2) - (origin.height / 2));
return toBottom ? POSITION.BOTTOM : POSITION.TOP;
@ -106,10 +140,7 @@ const tooltipFactory = (options = {}) => {
activate({ top, left, position }) {
if (this.timeout) clearTimeout(this.timeout);
this.setState({ visible: true, position });
this.timeout = setTimeout(() => {
this.setState({ active: true, top, left });
}, this.props.tooltipDelay);
this.setState({ active: false, visible: true, position, top, left });
}
deactivate() {
@ -123,32 +154,31 @@ const tooltipFactory = (options = {}) => {
}
calculatePosition(element) {
const position = this.getPosition(element);
const { top, left, height, width } = element.getBoundingClientRect();
const xOffset = window.scrollX || window.pageXOffset;
const yOffset = window.scrollY || window.pageYOffset;
const origin = element.getBoundingClientRect();
const position = this.getPosition(origin);
const { top, left, height, width } = origin;
if (position === POSITION.BOTTOM) {
return {
top: top + height + yOffset,
left: left + (width / 2) + xOffset,
top: top + height,
left: left + (width / 2),
position,
};
} else if (position === POSITION.TOP) {
return {
top: top + yOffset,
left: left + (width / 2) + xOffset,
top,
left: left + (width / 2),
position,
};
} else if (position === POSITION.LEFT) {
return {
top: top + (height / 2) + yOffset,
left: left + xOffset,
top: top + (height / 2),
left,
position,
};
} else if (position === POSITION.RIGHT) {
return {
top: top + (height / 2) + yOffset,
left: left + width + xOffset,
top: top + (height / 2),
left: left + width,
position,
};
}
@ -165,6 +195,36 @@ const tooltipFactory = (options = {}) => {
if (this.props.onMouseLeave) this.props.onMouseLeave(event);
};
handleMouseEnterForChildren = (event) => {
let el = event.target;
while (el && (!el.getAttribute || !el.getAttribute('tooltip'))) {
el = el.parentNode;
}
if (el) {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.setState({ tooltip: el.getAttribute('tooltip') });
const pos = this.calculatePosition(el);
if (!this.state.visible) {
this.activate(pos);
} else if (this.state.position !== pos.position ||
this.state.top !== pos.top || this.state.left !== pos.left) {
this.setState({ active: false, visible: false }, () => this.activate(pos));
}
}
};
handleMouseLeaveForChildren = () => {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.deactivate();
}, 300);
};
handleClick = (event) => {
if (this.props.tooltipHideOnClick && this.state.active) {
this.deactivate();
@ -178,7 +238,7 @@ const tooltipFactory = (options = {}) => {
};
render() {
const { active, left, top, position, visible } = this.state;
const { active, left, top, transform, position, visible } = this.state;
const positionClass = `tooltip${position.charAt(0).toUpperCase() + position.slice(1)}`;
const {
children,
@ -188,6 +248,8 @@ const tooltipFactory = (options = {}) => {
onMouseEnter, // eslint-disable-line no-unused-vars
onMouseLeave, // eslint-disable-line no-unused-vars
tooltip,
tooltipForChildren,
tooltipOnFocus, // eslint-disable-line no-unused-vars
tooltipDelay, // eslint-disable-line no-unused-vars
tooltipHideOnClick, // eslint-disable-line no-unused-vars
tooltipPosition, // eslint-disable-line no-unused-vars
@ -204,27 +266,39 @@ const tooltipFactory = (options = {}) => {
...other,
className,
onClick: this.handleClick,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave,
};
if (tooltipOnFocus) {
childProps.onFocus = this.handleMouseEnter;
childProps.onBlur = this.handleMouseLeave;
} else if (tooltipForChildren) {
childProps.onMouseOver = this.handleMouseEnterForChildren;
childProps.onMouseOut = this.handleMouseLeaveForChildren;
} else {
childProps.onMouseEnter = this.handleMouseEnter;
childProps.onMouseLeave = this.handleMouseLeave;
}
const shouldPass = typeof ComposedComponent !== 'string' && defaultPassthrough;
const finalProps = shouldPass ? { ...childProps, theme } : childProps;
return React.createElement(ComposedComponent, finalProps, children,
visible && (
return (<React.Fragment>
{React.createElement(ComposedComponent, finalProps, children)}
{visible && (
<Portal>
<span
ref={(node) => { this.tooltipNode = node; }}
ref={this.setTooltipNode}
className={_className}
data-react-toolbox="tooltip"
style={{ top, left }}
style={active ? { top, left, transform } : { top: '-1000px', left: 0 }}
>
<span className={theme.tooltipInner}>{tooltip}</span>
<span className={theme.tooltipInner}>
{this.state.tooltip || this.props.tooltip}
</span>
</span>
</Portal>
),
);
)}
</React.Fragment>);
}
}

View File

@ -1,10 +1,10 @@
:root {
--tooltip-background: color(rgb(97, 97, 97) a(90%));
--tooltip-margin: calc(0.5 * var(--unit));
--tooltip-border-radius: calc(0.2 * var(--unit));
--tooltip-border-radius: calc(0.5 * var(--unit));
--tooltip-color: var(--color-white);
--tooltip-font-size: var(--unit);
--tooltip-max-width: calc(17 * var(--unit));
--tooltip-padding: calc(0.8 * var(--unit));
--tooltip-font-size: calc(1.5 * var(--unit));
--tooltip-max-width: calc(35 * var(--unit));
--tooltip-padding: calc(1.2 * var(--unit));
--tooltip-animation-duration: 200ms;
}

View File

@ -7,11 +7,11 @@
font-family: var(--preferred-font);
font-size: var(--tooltip-font-size);
font-weight: var(--font-weight-bold);
line-height: var(--font-size-small);
line-height: calc(1.1 * var(--tooltip-font-size));
max-width: var(--tooltip-max-width);
padding: var(--tooltip-margin);
pointer-events: none;
position: absolute;
position: fixed;
text-align: center;
text-transform: none;
transform: scale(0) translateX(-50%);

View File

@ -2,7 +2,7 @@
"name": "react-toolbox",
"description": "A set of React components implementing Google's Material Design specification with the power of CSS Modules.",
"homepage": "http://www.react-toolbox.io",
"version": "2.0.0-beta.16",
"version": "2.0.0-beta.24",
"main": "./lib",
"module": "./components",
"author": {
@ -12,11 +12,11 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/react-toolbox/react-toolbox.git"
"url": "git+https://github.com/vitalif/react-toolbox.git"
},
"bugs": {
"email": "issues@react-toolbox.io",
"url": "https://github.com/react-toolbox/react-toolbox/issues"
"url": "https://github.com/vitalif/react-toolbox/issues"
},
"keywords": [
"components",