@spothero/ui 15.0.0 → 15.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -3
- package/styles/v2/components/FormControl/FormControl.jsx +20 -2
- package/styles/v2/components/Radio/Radio.jsx +50 -0
- package/styles/v2/components/Radio/Radio.stories.js +155 -0
- package/styles/v2/components/Radio/RadioGroup.jsx +69 -0
- package/styles/v2/components/Radio/styles/index.js +52 -0
- package/styles/v2/components/index.js +1 -0
- package/styles/v2/components/styles.js +1 -0
- package/v2/index.js +1 -1
- package/v2/index.js.map +1 -1
- package/styles/Alert/Alert.jsx +0 -45
- package/styles/Alert/Alert.spec.js +0 -85
- package/styles/AutoSuggestInput/AutoSuggestInput.jsx +0 -429
- package/styles/AutoSuggestInput/AutoSuggestInput.spec.js +0 -132
- package/styles/AutoSuggestInput/AutoSuggestItem.jsx +0 -61
- package/styles/AutoSuggestInput/AutoSuggestList.jsx +0 -85
- package/styles/Badge/Badge.jsx +0 -24
- package/styles/Badge/Badge.spec.js +0 -43
- package/styles/Chart/Chart.jsx +0 -185
- package/styles/Chart/Chart.spec.js +0 -369
- package/styles/Checkbox/Checkbox.jsx +0 -159
- package/styles/Checkbox/Checkbox.spec.js +0 -142
- package/styles/DateTime/DatePicker.jsx +0 -281
- package/styles/DateTime/DatePicker.spec.js +0 -186
- package/styles/DateTime/DatePickerCalendar.jsx +0 -170
- package/styles/DateTime/DatePickerCalendarNavigation.jsx +0 -44
- package/styles/DateTime/DatePickerCalendarWithRange.jsx +0 -218
- package/styles/DateTime/DateTimePicker.jsx +0 -266
- package/styles/DateTime/DateTimePicker.spec.js +0 -60
- package/styles/DateTime/DateTimeRangePicker.jsx +0 -629
- package/styles/DateTime/DateTimeRangePicker.spec.js +0 -425
- package/styles/DateTime/TimePicker.jsx +0 -158
- package/styles/DateTime/TimePicker.spec.js +0 -148
- package/styles/DateTime/date-time-assertions.js +0 -89
- package/styles/DateTime/index.js +0 -6
- package/styles/ErrorBoundary/ErrorBoundary.jsx +0 -76
- package/styles/ErrorBoundary/ErrorBoundary.spec.js +0 -72
- package/styles/Flyout/Flyout.jsx +0 -147
- package/styles/Flyout/Flyout.spec.js +0 -117
- package/styles/Form/Form.jsx +0 -151
- package/styles/Form/Form.spec.js +0 -148
- package/styles/Form/FormElementError.jsx +0 -18
- package/styles/Form/FormGroup.jsx +0 -32
- package/styles/Form/FormGroupError.jsx +0 -24
- package/styles/Form/index.js +0 -4
- package/styles/GooglePlacesSearchInput/GooglePlacesSearchInput.jsx +0 -215
- package/styles/GooglePlacesSearchInput/GooglePlacesSearchInput.spec.js +0 -213
- package/styles/GooglePlacesSearchInput/PoweredByGoogle.jsx +0 -43
- package/styles/GooglePlacesSearchInput/index.js +0 -2
- package/styles/HorizontalRule/HorizontalRule.jsx +0 -36
- package/styles/HorizontalRule/HorizontalRule.spec.js +0 -94
- package/styles/Label/Label.jsx +0 -22
- package/styles/Label/Label.spec.js +0 -11
- package/styles/Notification/Notification.jsx +0 -117
- package/styles/Notification/Notification.spec.js +0 -154
- package/styles/Notification/NotificationContainer.jsx +0 -90
- package/styles/Notification/NotificationPropTypes.js +0 -20
- package/styles/Notification/index.js +0 -2
- package/styles/PasswordControl/PasswordControl.jsx +0 -197
- package/styles/PasswordControl/PasswordControl.spec.js +0 -236
- package/styles/Portal/Portal.jsx +0 -65
- package/styles/Portal/Portal.spec.js +0 -45
- package/styles/PulseLoader/PulseLoader.jsx +0 -71
- package/styles/PulseLoader/PulseLoader.spec.js +0 -63
- package/styles/Radio/Radio.jsx +0 -114
- package/styles/Radio/Radio.spec.js +0 -128
- package/styles/Radio/RadioGroup.jsx +0 -105
- package/styles/RenderInBody/RenderInBody.jsx +0 -56
- package/styles/RenderInBody/RenderInBody.spec.js +0 -24
- package/styles/Select/Select.jsx +0 -251
- package/styles/Select/Select.spec.js +0 -254
- package/styles/Select/SelectItemPropTypes.js +0 -19
- package/styles/Select/index.js +0 -2
- package/styles/SelectControlled/SelectControlled.jsx +0 -250
- package/styles/SelectControlled/SelectControlled.spec.js +0 -290
- package/styles/SelectControlled/index.js +0 -1
- package/styles/Sprite/Sprite.jsx +0 -16
- package/styles/Sprite/Sprite.spec.js +0 -11
- package/styles/Tabs/Tab.jsx +0 -38
- package/styles/Tabs/TabContent.jsx +0 -46
- package/styles/Tabs/TabNavigation.jsx +0 -64
- package/styles/Tabs/TabPanel.jsx +0 -30
- package/styles/Tabs/Tabs.jsx +0 -87
- package/styles/Tabs/Tabs.spec.js +0 -201
- package/styles/Tabs/index.js +0 -5
- package/styles/TextArea/TextArea.jsx +0 -137
- package/styles/TextArea/TextArea.spec.js +0 -111
- package/styles/TextInput/TextInput.jsx +0 -159
- package/styles/TextInput/TextInput.spec.js +0 -263
- package/styles/TextInput/TextInputPropTypes.js +0 -88
- package/styles/TextInput/index.js +0 -2
- package/styles/Tooltip/Tooltip.jsx +0 -230
- package/styles/Tooltip/Tooltip.spec.js +0 -170
- /package/styles/{Radio → v2/components/Radio}/index.js +0 -0
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
describe('<TextInput />', () => {
|
|
2
|
-
context('Display', () => {
|
|
3
|
-
context('Default', () => {
|
|
4
|
-
it('Should have placeholder, default value, label, help text, and custom class names', () => {
|
|
5
|
-
cy.visitStory('v1/TextInput/Display', 'Default')
|
|
6
|
-
.get('.TextInput')
|
|
7
|
-
.should('exist')
|
|
8
|
-
.and('have.class', 'custom-class1')
|
|
9
|
-
.and('have.class', 'custom-class2')
|
|
10
|
-
.find('input')
|
|
11
|
-
.should('have.attr', 'name', 'license-plate')
|
|
12
|
-
.and('have.attr', 'placeholder', 'License Plate')
|
|
13
|
-
.and('have.value', 'ECTO-1')
|
|
14
|
-
.get('.TextInput')
|
|
15
|
-
.find('.Label')
|
|
16
|
-
.contains('Add new license plate')
|
|
17
|
-
.get('.TextInput')
|
|
18
|
-
.find('.FormElement-help-text')
|
|
19
|
-
.contains('Additional help node text');
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
context('Disabled', () => {
|
|
24
|
-
it('Is disabled', () => {
|
|
25
|
-
cy.visitStory('v1/TextInput/Display', 'Disabled')
|
|
26
|
-
.get('.TextInput')
|
|
27
|
-
.find('input')
|
|
28
|
-
.should('be.disabled');
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
context('Read Only', () => {
|
|
33
|
-
it('Is read only', () => {
|
|
34
|
-
cy.visitStory('v1/TextInput/Display', 'ReadOnly')
|
|
35
|
-
.get('.TextInput')
|
|
36
|
-
.find('input')
|
|
37
|
-
.should('have.attr', 'readonly');
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
context('Icon', () => {
|
|
42
|
-
it('Should render with an icon', () => {
|
|
43
|
-
cy.visitStory('v1/TextInput/Display', 'Icon')
|
|
44
|
-
.get('.TextInput')
|
|
45
|
-
.should('have.class', 'FormElement-with-icon-right')
|
|
46
|
-
.find('.Icon')
|
|
47
|
-
.should('exist');
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
context('Loading', () => {
|
|
52
|
-
it('Should render with a loading spinner', () => {
|
|
53
|
-
cy.visitStory('v1/TextInput/Display', 'Loading')
|
|
54
|
-
.get('.TextInput')
|
|
55
|
-
.find('.Loader')
|
|
56
|
-
.should('exist');
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
context('Inline', () => {
|
|
61
|
-
it('Should render with inline styling', () => {
|
|
62
|
-
cy.visitStory('v1/TextInput/Display', 'Inline')
|
|
63
|
-
.get('.TextInput')
|
|
64
|
-
.should('have.class', 'FormElement-inline');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
context('Auto Focus', () => {
|
|
69
|
-
it('Receives focus after rendered', () => {
|
|
70
|
-
cy.visitStory('v1/TextInput/Display', 'AutoFocus')
|
|
71
|
-
.focused()
|
|
72
|
-
.should('have.attr', 'name', 'license-plate');
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
context('Formatting', () => {
|
|
78
|
-
describe('Mask', () => {
|
|
79
|
-
it('Applies a mask over the input text', () => {
|
|
80
|
-
cy.visitStory('v1/TextInput/Formatting', 'Mask')
|
|
81
|
-
.get('.TextInput')
|
|
82
|
-
.find('input')
|
|
83
|
-
.click()
|
|
84
|
-
.should('have.value', '+X-XXX-XXX-XXXX')
|
|
85
|
-
.type('12345678899', {delay: 0})
|
|
86
|
-
.blur()
|
|
87
|
-
.should('have.value', '+1-234-567-8899');
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('Pattern', () => {
|
|
92
|
-
it('Has pattern attribute and ensures the input text follows the specified regex pattern', () => {
|
|
93
|
-
cy.visitStory('v1/TextInput/Formatting', 'Pattern')
|
|
94
|
-
.get('.TextInput')
|
|
95
|
-
.find('input')
|
|
96
|
-
.should('have.attr', 'pattern', '[A-Za-z0-9]+')
|
|
97
|
-
// should show error since value does not follow pattern
|
|
98
|
-
.type('OÜTAT!M&', {delay: 0})
|
|
99
|
-
.get('button[type=submit]')
|
|
100
|
-
.click()
|
|
101
|
-
.get('.TextInput')
|
|
102
|
-
.find('.FormElementError')
|
|
103
|
-
.should('exist')
|
|
104
|
-
// should not show error since value follows pattern
|
|
105
|
-
.get('.TextInput')
|
|
106
|
-
.find('input')
|
|
107
|
-
.clear()
|
|
108
|
-
.type('oUtAT1M3', {delay: 0})
|
|
109
|
-
.get('button[type=submit]')
|
|
110
|
-
.click()
|
|
111
|
-
.get('.TextInput')
|
|
112
|
-
.find('.FormElementError')
|
|
113
|
-
.should('not.exist');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
context('Max Length', () => {
|
|
118
|
-
it('Has maxlength attribute and does not allow more characters', () => {
|
|
119
|
-
// longstring is 53 characters long
|
|
120
|
-
const longstring =
|
|
121
|
-
'myusernamemyusernamemyusernamemyusernamemyusernameabc';
|
|
122
|
-
|
|
123
|
-
cy.visitStory('v1/TextInput/Formatting', 'MaxLength')
|
|
124
|
-
.get('.TextInput')
|
|
125
|
-
.find('input')
|
|
126
|
-
.should('have.attr', 'maxlength', '50')
|
|
127
|
-
.type(longstring, {delay: 0})
|
|
128
|
-
.should('have.value', longstring.substring(0, 50));
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
context('Min Max', () => {
|
|
133
|
-
it('Has min and max attributes and does not consider input outside that range valid', () => {
|
|
134
|
-
cy.visitStory('v1/TextInput/Formatting', 'MinMax')
|
|
135
|
-
.get('.TextInput')
|
|
136
|
-
.find('input')
|
|
137
|
-
.should('have.attr', 'min', '1')
|
|
138
|
-
.should('have.attr', 'max', '100')
|
|
139
|
-
// should show error since value is below min
|
|
140
|
-
.type('0', {delay: 0})
|
|
141
|
-
.get('button[type=submit]')
|
|
142
|
-
.click()
|
|
143
|
-
.get('.TextInput')
|
|
144
|
-
.find('.FormElementError')
|
|
145
|
-
.should('exist')
|
|
146
|
-
// should not show error since value is in range
|
|
147
|
-
.get('.TextInput')
|
|
148
|
-
.find('input')
|
|
149
|
-
.clear()
|
|
150
|
-
.type('42', {delay: 0})
|
|
151
|
-
.get('button[type=submit]')
|
|
152
|
-
.click()
|
|
153
|
-
.get('.TextInput')
|
|
154
|
-
.find('.FormElementError')
|
|
155
|
-
.should('not.exist')
|
|
156
|
-
// should show error since value is above max
|
|
157
|
-
.get('.TextInput')
|
|
158
|
-
.find('input')
|
|
159
|
-
.clear()
|
|
160
|
-
.type('4200', {delay: 0})
|
|
161
|
-
.get('button[type=submit]')
|
|
162
|
-
.click()
|
|
163
|
-
.get('.TextInput')
|
|
164
|
-
.find('.FormElementError')
|
|
165
|
-
.should('exist');
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
context('Methods', () => {
|
|
171
|
-
describe('On Change', () => {
|
|
172
|
-
it('Executes the method when a change is detected', () => {
|
|
173
|
-
cy.visitStory('v1/TextInput/Methods', 'OnChange', {
|
|
174
|
-
onBeforeLoad: contentWindow => {
|
|
175
|
-
contentWindow.onTextInputChange = evt => {
|
|
176
|
-
console.log(`onTextInputChange`); // eslint-disable-line no-console
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
cy.stub(contentWindow, 'onTextInputChange').as(
|
|
180
|
-
'onTextInputChange'
|
|
181
|
-
);
|
|
182
|
-
},
|
|
183
|
-
})
|
|
184
|
-
.get('.TextInput')
|
|
185
|
-
.find('input')
|
|
186
|
-
.type('OUTATIME', {delay: 0})
|
|
187
|
-
.get('@onTextInputChange')
|
|
188
|
-
.should('be.called');
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe('On Focus', () => {
|
|
193
|
-
it('Executes the method when input is focused on', () => {
|
|
194
|
-
cy.visitStory('v1/TextInput/Methods', 'OnFocus', {
|
|
195
|
-
onBeforeLoad: contentWindow => {
|
|
196
|
-
contentWindow.onTextInputFocus = evt => {
|
|
197
|
-
console.log(`onTextInputFocus`); // eslint-disable-line no-console
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
cy.stub(contentWindow, 'onTextInputFocus').as(
|
|
201
|
-
'onTextInputFocus'
|
|
202
|
-
);
|
|
203
|
-
},
|
|
204
|
-
})
|
|
205
|
-
.get('.TextInput')
|
|
206
|
-
.find('input')
|
|
207
|
-
.focus()
|
|
208
|
-
.get('@onTextInputFocus')
|
|
209
|
-
.should('be.called');
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
describe('On Blur', () => {
|
|
214
|
-
it('Executes the method when input loses focus', () => {
|
|
215
|
-
cy.visitStory('v1/TextInput/Methods', 'OnBlur', {
|
|
216
|
-
onBeforeLoad: contentWindow => {
|
|
217
|
-
contentWindow.onTextInputBlur = evt => {
|
|
218
|
-
console.log(`onTextInputBlur`); // eslint-disable-line no-console
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
cy.stub(contentWindow, 'onTextInputBlur').as(
|
|
222
|
-
'onTextInputBlur'
|
|
223
|
-
);
|
|
224
|
-
},
|
|
225
|
-
})
|
|
226
|
-
.get('.TextInput')
|
|
227
|
-
.find('input')
|
|
228
|
-
.focus()
|
|
229
|
-
.get('@onTextInputBlur')
|
|
230
|
-
.should('not.be.called')
|
|
231
|
-
.get('.TextInput')
|
|
232
|
-
.find('input')
|
|
233
|
-
.blur()
|
|
234
|
-
.get('@onTextInputBlur')
|
|
235
|
-
.should('be.called');
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
describe('On Invalid', () => {
|
|
240
|
-
it('Displays an error if the input is invalid', () => {
|
|
241
|
-
cy.visitStory('v1/TextInput/Methods', 'OnInvalid')
|
|
242
|
-
.get('.TextInput')
|
|
243
|
-
.find('input')
|
|
244
|
-
// Show error on empty field since this is required
|
|
245
|
-
.should('have.value', '')
|
|
246
|
-
.get('button[type=submit]')
|
|
247
|
-
.click()
|
|
248
|
-
.get('.TextInput')
|
|
249
|
-
.find('.FormElementError')
|
|
250
|
-
.should('exist')
|
|
251
|
-
// Don't show error on field with content
|
|
252
|
-
.get('.TextInput')
|
|
253
|
-
.find('input')
|
|
254
|
-
.type('MY PRSHE', {delay: 0})
|
|
255
|
-
.get('button[type=submit]')
|
|
256
|
-
.click()
|
|
257
|
-
.get('.TextInput')
|
|
258
|
-
.find('.FormElementError')
|
|
259
|
-
.should('not.exist');
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import PropTypes from 'prop-types';
|
|
2
|
-
|
|
3
|
-
const TextInputPropTypes = {
|
|
4
|
-
/** Additional class(es) to add to the component. */
|
|
5
|
-
className: PropTypes.string,
|
|
6
|
-
/** A unique name which is used to identify this element. If belonging to a form this is used as the key for serialization and is required. */
|
|
7
|
-
name: PropTypes.string,
|
|
8
|
-
/** What HTML input type the underlying input should be set to. */
|
|
9
|
-
type: PropTypes.oneOf([
|
|
10
|
-
'text',
|
|
11
|
-
'password',
|
|
12
|
-
'date',
|
|
13
|
-
'email',
|
|
14
|
-
'time',
|
|
15
|
-
'tel',
|
|
16
|
-
'search',
|
|
17
|
-
'number',
|
|
18
|
-
'hidden',
|
|
19
|
-
]),
|
|
20
|
-
/** Text to display in the field before a user starts to type in the input. */
|
|
21
|
-
placeholder: PropTypes.string,
|
|
22
|
-
/** A custom label (typically a Label component) to display above the input. */
|
|
23
|
-
label: PropTypes.element,
|
|
24
|
-
/** A regular expression that the input value is checked against for validation. */
|
|
25
|
-
pattern: PropTypes.string,
|
|
26
|
-
/** Whether this field is required during validation. */
|
|
27
|
-
required: PropTypes.bool,
|
|
28
|
-
/** Disallows user interaction. */
|
|
29
|
-
disabled: PropTypes.bool,
|
|
30
|
-
/** Whether to show a loading animation inside of the component. */
|
|
31
|
-
loading: PropTypes.bool,
|
|
32
|
-
/** Whether the field should be auto focused when mounted. */
|
|
33
|
-
autoFocus: PropTypes.bool,
|
|
34
|
-
/** Whether the field should be focused when the component receives new props. */
|
|
35
|
-
focus: PropTypes.bool,
|
|
36
|
-
/** A custom validation error (typically a FormElementError component) to display when this component is part of a form and is invalid. */
|
|
37
|
-
error: PropTypes.element,
|
|
38
|
-
/** The default value to display in the input when mounted. */
|
|
39
|
-
defaultValue: PropTypes.string,
|
|
40
|
-
/** A custom icon (typically an Icon component) to show in the input field. */
|
|
41
|
-
icon: PropTypes.element,
|
|
42
|
-
/** If an icon is provided, whether it should appear on the left or right side of the input field. */
|
|
43
|
-
iconPosition: PropTypes.oneOf(['left', 'right']),
|
|
44
|
-
/** A custom help element to place below the input. */
|
|
45
|
-
helpNode: PropTypes.node,
|
|
46
|
-
/** Applies proper styling to display this field inline in a form. */
|
|
47
|
-
inline: PropTypes.bool,
|
|
48
|
-
/** Specifies that the input field is read-only. */
|
|
49
|
-
readOnly: PropTypes.bool,
|
|
50
|
-
/** Specifies the maximum number of characters allowed. */
|
|
51
|
-
maxLength: PropTypes.number,
|
|
52
|
-
/** Specifies the minimum value when type is `date` or `number`. */
|
|
53
|
-
min: PropTypes.number,
|
|
54
|
-
/** Specifies the maximum value when type is `date` or `number`. */
|
|
55
|
-
max: PropTypes.number,
|
|
56
|
-
/** A string to use as a mask. See <a href="https://github.com/sanniassin/react-input-mask#mask--string" target="_blank">react-input-mask</a>. */
|
|
57
|
-
mask: PropTypes.string,
|
|
58
|
-
/** Character to cover unfilled editable parts of mask. See <a href="https://github.com/sanniassin/react-input-mask#maskchar--string" target="_blank">react-input-mask</a>. */
|
|
59
|
-
maskChar: PropTypes.string,
|
|
60
|
-
/** Additional props to pass to the input element */
|
|
61
|
-
additionalInputProps: PropTypes.object,
|
|
62
|
-
/**
|
|
63
|
-
* Function to execute when the input changes.
|
|
64
|
-
*
|
|
65
|
-
* @param {SyntheticEvent} evt - The React `SyntheticEvent`.
|
|
66
|
-
*/
|
|
67
|
-
onChange: PropTypes.func,
|
|
68
|
-
/**
|
|
69
|
-
* Function to execute when the input is focused.
|
|
70
|
-
*
|
|
71
|
-
* @param {SyntheticEvent} evt - The React `SyntheticEvent`.
|
|
72
|
-
*/
|
|
73
|
-
onFocus: PropTypes.func,
|
|
74
|
-
/**
|
|
75
|
-
* Function to execute when the input loses focused.
|
|
76
|
-
*
|
|
77
|
-
* @param {SyntheticEvent} evt - The React `SyntheticEvent`.
|
|
78
|
-
*/
|
|
79
|
-
onBlur: PropTypes.func,
|
|
80
|
-
/**
|
|
81
|
-
* Function to execute when the element is marked as invalid by a validation check.
|
|
82
|
-
*
|
|
83
|
-
* @param {ValidityState} validationState - ValidityState object for more fine grained control over error handling in a parent component if desired.
|
|
84
|
-
*/
|
|
85
|
-
onInvalid: PropTypes.func,
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export default TextInputPropTypes;
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import bind from 'lodash/bind';
|
|
2
|
-
import React, {Component} from 'react';
|
|
3
|
-
import PropTypes from 'prop-types';
|
|
4
|
-
import classNames from 'classnames';
|
|
5
|
-
import IconTimes from '@spothero/icons/times';
|
|
6
|
-
import DOMUtils from '@spothero/utils/dom';
|
|
7
|
-
import Button from 'v1/components/Button/Button';
|
|
8
|
-
import Portal from '../Portal/Portal';
|
|
9
|
-
|
|
10
|
-
export default class Tooltip extends Component {
|
|
11
|
-
static propTypes = {
|
|
12
|
-
/** Additional class(es) to add to the component. */
|
|
13
|
-
className: PropTypes.string,
|
|
14
|
-
/** Additional class(es) to add to the containing Portal component. */
|
|
15
|
-
containerClassName: PropTypes.string,
|
|
16
|
-
/** The markup node to insert into the tooltip. */
|
|
17
|
-
children: PropTypes.node.isRequired,
|
|
18
|
-
/** Where to position the tooltip in relation to the triggering element. */
|
|
19
|
-
position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
|
20
|
-
/** Whether to show the close button or not. */
|
|
21
|
-
showClose: PropTypes.bool,
|
|
22
|
-
/** The selector (.foo, #foo, etc...) for the element to attach the tooltip to. */
|
|
23
|
-
triggerSelector: PropTypes.string.isRequired,
|
|
24
|
-
/** Whether to reposition the tooltip when it moves, be it via window resize or scrolling or whatever moves it. */
|
|
25
|
-
updateOnMove: PropTypes.bool,
|
|
26
|
-
/** The offset of the tooltip from the triggering element. This is based on what the position is set to (`top` offsets up, etc). */
|
|
27
|
-
tipOffset: PropTypes.number,
|
|
28
|
-
/** Whether to automatically adjust the tooltip if its off the left or right side of the screen. */
|
|
29
|
-
adjustArrow: PropTypes.bool,
|
|
30
|
-
/** Whether to close the tooltip if the body outside of it is clicked. */
|
|
31
|
-
closeOnBodyClick: PropTypes.bool,
|
|
32
|
-
/** A function to execute after the tooltip has been hidden. Use this to manage state in a parent component to remove the tooltip from the DOM (see examples). */
|
|
33
|
-
onHidden: PropTypes.func.isRequired,
|
|
34
|
-
};
|
|
35
|
-
static defaultProps = {
|
|
36
|
-
position: 'top',
|
|
37
|
-
showClose: true,
|
|
38
|
-
tipOffset: 0,
|
|
39
|
-
adjustArrow: true,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
constructor(props) {
|
|
43
|
-
super(props);
|
|
44
|
-
|
|
45
|
-
this._isArrowAdjusted = false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
componentDidMount() {
|
|
49
|
-
const {triggerSelector, updateOnMove, closeOnBodyClick} = this.props;
|
|
50
|
-
|
|
51
|
-
this._trigger = DOMUtils.el(triggerSelector);
|
|
52
|
-
this._positionTooltip();
|
|
53
|
-
|
|
54
|
-
DOMUtils.addClass(this._container, 'Tooltip-showing');
|
|
55
|
-
|
|
56
|
-
if (updateOnMove) {
|
|
57
|
-
this._container.style.position = 'fixed';
|
|
58
|
-
|
|
59
|
-
this._scrollInterval = setInterval(
|
|
60
|
-
bind(this._positionTooltip, this),
|
|
61
|
-
100
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (closeOnBodyClick) {
|
|
66
|
-
document.addEventListener('click', this._onCloseClick, {
|
|
67
|
-
capture: true,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
componentWillUnmount() {
|
|
73
|
-
const {updateOnMove, closeOnBodyClick} = this.props;
|
|
74
|
-
|
|
75
|
-
if (updateOnMove) {
|
|
76
|
-
clearInterval(this._scrollInterval);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (closeOnBodyClick) {
|
|
80
|
-
document.removeEventListener('click', this._onCloseClick);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
_onCloseClick = evt => {
|
|
85
|
-
this.hide();
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
_positionTooltip() {
|
|
89
|
-
if (!this._trigger) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const {position, tipOffset, updateOnMove, adjustArrow} = this.props;
|
|
94
|
-
const {
|
|
95
|
-
top: clientTop,
|
|
96
|
-
left: clientLeft,
|
|
97
|
-
right: clientRight,
|
|
98
|
-
bottom: clientBottom,
|
|
99
|
-
width,
|
|
100
|
-
height,
|
|
101
|
-
} = this._trigger.getBoundingClientRect();
|
|
102
|
-
const tooltipWidth = this._container.offsetWidth;
|
|
103
|
-
const tooltipHeight = this._container.offsetHeight;
|
|
104
|
-
const {
|
|
105
|
-
top: documentTop,
|
|
106
|
-
left: documentLeft,
|
|
107
|
-
} = DOMUtils.getDocumentOffset(this._trigger);
|
|
108
|
-
const top = updateOnMove ? clientTop : documentTop;
|
|
109
|
-
const right = updateOnMove ? clientRight : documentLeft + width;
|
|
110
|
-
const bottom = updateOnMove ? clientBottom : documentTop + height;
|
|
111
|
-
const left = updateOnMove ? clientLeft : documentLeft;
|
|
112
|
-
const containerMid =
|
|
113
|
-
position === 'top' || position === 'bottom'
|
|
114
|
-
? tooltipWidth / 2
|
|
115
|
-
: tooltipHeight / 2;
|
|
116
|
-
const bodyWidth = document.body.clientWidth;
|
|
117
|
-
const adjuster = 5;
|
|
118
|
-
let newTop = 0;
|
|
119
|
-
let newLeft = 0;
|
|
120
|
-
|
|
121
|
-
switch (position) {
|
|
122
|
-
case 'top':
|
|
123
|
-
newTop = top - tooltipHeight - tipOffset;
|
|
124
|
-
newLeft = left + width / 2 - containerMid;
|
|
125
|
-
break;
|
|
126
|
-
|
|
127
|
-
case 'right':
|
|
128
|
-
newTop = top + height / 2 - containerMid;
|
|
129
|
-
newLeft = right + tipOffset;
|
|
130
|
-
break;
|
|
131
|
-
|
|
132
|
-
case 'bottom':
|
|
133
|
-
newTop = bottom + tipOffset;
|
|
134
|
-
newLeft = left + width / 2 - containerMid;
|
|
135
|
-
break;
|
|
136
|
-
|
|
137
|
-
case 'left':
|
|
138
|
-
newTop = top + height / 2 - containerMid;
|
|
139
|
-
newLeft = left - tooltipWidth - tipOffset;
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// adjust tooltip position if off the left side of the screen
|
|
144
|
-
let adjustedLeft = newLeft < 0 ? adjuster : newLeft;
|
|
145
|
-
|
|
146
|
-
// adjust tooltip position if off the right side of the screen
|
|
147
|
-
if (tooltipWidth > bodyWidth - adjustedLeft) {
|
|
148
|
-
adjustedLeft = bodyWidth - tooltipWidth - adjuster;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
this._container.style.top = `${newTop}px`;
|
|
152
|
-
this._container.style.left = `${adjustedLeft}px`;
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
adjustArrow &&
|
|
156
|
-
!this._isArrowAdjusted &&
|
|
157
|
-
adjustedLeft !== newLeft &&
|
|
158
|
-
(position === 'top' || position === 'bottom')
|
|
159
|
-
) {
|
|
160
|
-
this._arrow.style.left = `${
|
|
161
|
-
this._arrow.getBoundingClientRect().left +
|
|
162
|
-
(newLeft - adjustedLeft)
|
|
163
|
-
}px`;
|
|
164
|
-
|
|
165
|
-
this._isArrowAdjusted = true;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Hides the Tooltip programmatically and fires the `onHidden` callback when done.
|
|
171
|
-
*
|
|
172
|
-
* @public
|
|
173
|
-
* @returns {void}
|
|
174
|
-
*/
|
|
175
|
-
hide() {
|
|
176
|
-
if (DOMUtils.hasClass(this._container, 'Tooltip-showing')) {
|
|
177
|
-
DOMUtils.removeClass(this._container, 'Tooltip-showing');
|
|
178
|
-
|
|
179
|
-
setTimeout(() => {
|
|
180
|
-
this.props.onHidden();
|
|
181
|
-
}, 250);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
render() {
|
|
186
|
-
const {
|
|
187
|
-
className,
|
|
188
|
-
containerClassName,
|
|
189
|
-
position,
|
|
190
|
-
showClose,
|
|
191
|
-
children,
|
|
192
|
-
} = this.props;
|
|
193
|
-
const classes = classNames(
|
|
194
|
-
'Tooltip',
|
|
195
|
-
`Tooltip-${position}`,
|
|
196
|
-
{'Tooltip-no-close': !showClose},
|
|
197
|
-
className
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
return (
|
|
201
|
-
<Portal className={containerClassName}>
|
|
202
|
-
<div
|
|
203
|
-
ref={node => {
|
|
204
|
-
this._container = node;
|
|
205
|
-
}}
|
|
206
|
-
className={classes}
|
|
207
|
-
role="tooltip"
|
|
208
|
-
>
|
|
209
|
-
<div
|
|
210
|
-
ref={node => {
|
|
211
|
-
this._arrow = node;
|
|
212
|
-
}}
|
|
213
|
-
className="Tooltip-arrow"
|
|
214
|
-
/>
|
|
215
|
-
<div className="Tooltip-content">
|
|
216
|
-
{children}
|
|
217
|
-
{showClose && (
|
|
218
|
-
<Button
|
|
219
|
-
className="Tooltip-close"
|
|
220
|
-
onClick={this._onCloseClick}
|
|
221
|
-
>
|
|
222
|
-
<IconTimes />
|
|
223
|
-
</Button>
|
|
224
|
-
)}
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
</Portal>
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
}
|