@invoicebox/ui 0.1.0-alpha.322 → 0.1.0-alpha.325
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/dist/components/form/CountrySelect/components/CountryItem/index.js +3 -1
- package/dist/components/form/CountrySelect/index.d.ts +6 -0
- package/dist/components/form/CountrySelect/index.js +15 -4
- package/dist/components/form/CountrySelect/index.test.d.ts +1 -0
- package/dist/components/form/CountrySelect/index.test.js +85 -0
- package/dist/components/form/DateInput/index.d.ts +1 -1
- package/dist/components/form/DateInput/index.js +7 -1
- package/dist/components/form/DateInput/index.test.d.ts +1 -0
- package/dist/components/form/DateInput/index.test.js +71 -0
- package/dist/components/form/PhoneInput/index.d.ts +1 -1
- package/dist/components/form/PhoneInput/index.js +2 -1
- package/dist/components/form/PositiveIntegerInput/index.d.ts +4 -0
- package/dist/components/form/PositiveIntegerInput/index.js +46 -6
- package/dist/components/form/PositiveIntegerInput/index.test.d.ts +1 -0
- package/dist/components/form/PositiveIntegerInput/index.test.js +81 -0
- package/dist/components/form/PositiveIntegerInput/styles.d.ts +1 -3
- package/dist/components/form/PositiveIntegerInput/styles.js +8 -10
- package/dist/components/form/Select/index.js +23 -6
- package/dist/esm/components/form/CountrySelect/components/CountryItem/index.js +3 -1
- package/dist/esm/components/form/CountrySelect/index.js +16 -5
- package/dist/esm/components/form/CountrySelect/index.test.js +82 -0
- package/dist/esm/components/form/DateInput/index.js +7 -1
- package/dist/esm/components/form/DateInput/index.test.js +69 -0
- package/dist/esm/components/form/PhoneInput/index.js +2 -1
- package/dist/esm/components/form/PositiveIntegerInput/index.js +46 -6
- package/dist/esm/components/form/PositiveIntegerInput/index.test.js +78 -0
- package/dist/esm/components/form/PositiveIntegerInput/styles.js +7 -7
- package/dist/esm/components/form/Select/index.js +23 -6
- package/package.json +1 -1
|
@@ -27,7 +27,9 @@ var CountryItem = exports.CountryItem = function CountryItem(_ref) {
|
|
|
27
27
|
return /*#__PURE__*/_react["default"].createElement(S.Wrapper, {
|
|
28
28
|
$palette: palette,
|
|
29
29
|
onClick: handleSelect,
|
|
30
|
-
type: "button"
|
|
30
|
+
type: "button",
|
|
31
|
+
role: "option",
|
|
32
|
+
"aria-selected": isSelected
|
|
31
33
|
}, /*#__PURE__*/_react["default"].createElement(S.Title, {
|
|
32
34
|
$isSelected: isSelected,
|
|
33
35
|
$palette: palette
|
|
@@ -17,6 +17,12 @@ type TFieldProps = {
|
|
|
17
17
|
type TControlProps<T> = Pick<TSearchInputProps, 'placeholder'> & Pick<TCountryItemProps<T>, 'selectedLabel'> & {
|
|
18
18
|
options: TOption[];
|
|
19
19
|
disabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Только для скринридеров, на экране не выводится: начало aria-label кнопки выбора страны.
|
|
22
|
+
* Итоговое имя — «<buttonAriaLabelPrefix>: <название выбранной страны>», например
|
|
23
|
+
* «Код страны: Россия». Кнопка показывает только флаг, без имени её не понять (WCAG 4.1.2).
|
|
24
|
+
*/
|
|
25
|
+
buttonAriaLabelPrefix?: string;
|
|
20
26
|
} & Pick<TDropdownProps, 'positionVertical' | 'positionLeft' | 'positionRight' | 'minWidth' | 'width' | 'isAutoPosition' | 'isTopPosition'>;
|
|
21
27
|
export type TProps = TFieldProps & TControlProps<string>;
|
|
22
28
|
export declare const CountrySelect: FC<TProps>;
|
|
@@ -16,7 +16,7 @@ var _Arrow = require("../../common/Arrow");
|
|
|
16
16
|
var _Dropdown = require("../../common/Dropdown");
|
|
17
17
|
var _Scrollbar = require("../../common/Scrollbar");
|
|
18
18
|
var _palette = require("../../../palette");
|
|
19
|
-
var _excluded = ["value", "onChange", "options", "placeholder", "disabled", "selectedLabel"];
|
|
19
|
+
var _excluded = ["value", "onChange", "options", "placeholder", "disabled", "selectedLabel", "buttonAriaLabelPrefix"];
|
|
20
20
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
|
|
21
21
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
|
|
22
22
|
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
@@ -40,7 +40,10 @@ var CountrySelect = exports.CountrySelect = function CountrySelect(_ref) {
|
|
|
40
40
|
_ref$disabled = _ref.disabled,
|
|
41
41
|
disabled = _ref$disabled === void 0 ? false : _ref$disabled,
|
|
42
42
|
selectedLabel = _ref.selectedLabel,
|
|
43
|
+
_ref$buttonAriaLabelP = _ref.buttonAriaLabelPrefix,
|
|
44
|
+
buttonAriaLabelPrefix = _ref$buttonAriaLabelP === void 0 ? 'Код страны' : _ref$buttonAriaLabelP,
|
|
43
45
|
dropdownPositionProps = _objectWithoutProperties(_ref, _excluded);
|
|
46
|
+
var listboxId = (0, _react.useId)();
|
|
44
47
|
var palette = (0, _palette.useComponentPalette)('countrySelect');
|
|
45
48
|
var _useState = (0, _react.useState)(false),
|
|
46
49
|
_useState2 = _slicedToArray(_useState, 2),
|
|
@@ -87,8 +90,12 @@ var CountrySelect = exports.CountrySelect = function CountrySelect(_ref) {
|
|
|
87
90
|
}, /*#__PURE__*/_react["default"].createElement(S.HeaderWrapper, {
|
|
88
91
|
type: "button",
|
|
89
92
|
onClick: handleDropdownTrigger,
|
|
90
|
-
disabled: disabled
|
|
91
|
-
|
|
93
|
+
disabled: disabled,
|
|
94
|
+
"aria-label": selectedOption ? "".concat(buttonAriaLabelPrefix, ": ").concat(selectedOption.label) : buttonAriaLabelPrefix,
|
|
95
|
+
"aria-haspopup": "listbox",
|
|
96
|
+
"aria-expanded": isOpen,
|
|
97
|
+
"aria-controls": isOpen ? listboxId : undefined
|
|
98
|
+
}, (selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.flag) && /*#__PURE__*/_react["default"].createElement(_Flag.Flag, {
|
|
92
99
|
isSmall: false,
|
|
93
100
|
flag: selectedOption.flag
|
|
94
101
|
}), !disabled && /*#__PURE__*/_react["default"].createElement(_Arrow.Arrow, {
|
|
@@ -108,6 +115,10 @@ var CountrySelect = exports.CountrySelect = function CountrySelect(_ref) {
|
|
|
108
115
|
value: search
|
|
109
116
|
})), /*#__PURE__*/_react["default"].createElement(_Scrollbar.Scrollbar, {
|
|
110
117
|
maxHeight: 192
|
|
118
|
+
}, /*#__PURE__*/_react["default"].createElement("div", {
|
|
119
|
+
role: "listbox",
|
|
120
|
+
id: listboxId,
|
|
121
|
+
"aria-label": buttonAriaLabelPrefix
|
|
111
122
|
}, filtredOptions.map(function (option) {
|
|
112
123
|
return /*#__PURE__*/_react["default"].createElement(_CountryItem.CountryItem, {
|
|
113
124
|
key: option.value,
|
|
@@ -121,5 +132,5 @@ var CountrySelect = exports.CountrySelect = function CountrySelect(_ref) {
|
|
|
121
132
|
hint: option.hint,
|
|
122
133
|
flag: option.flag
|
|
123
134
|
});
|
|
124
|
-
})))));
|
|
135
|
+
}))))));
|
|
125
136
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _react = _interopRequireDefault(require("react"));
|
|
4
|
+
var _server = require("react-dom/server");
|
|
5
|
+
var _ = require(".");
|
|
6
|
+
var _PhoneInput = require("../PhoneInput");
|
|
7
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
|
|
8
|
+
var options = [{
|
|
9
|
+
label: 'Россия',
|
|
10
|
+
value: 'RUS',
|
|
11
|
+
description: '+7',
|
|
12
|
+
flag: 'RUS'
|
|
13
|
+
}, {
|
|
14
|
+
label: 'Казахстан',
|
|
15
|
+
value: 'KAZ',
|
|
16
|
+
description: '+7',
|
|
17
|
+
flag: 'KAZ'
|
|
18
|
+
}];
|
|
19
|
+
|
|
20
|
+
// Серверный рендер: предупреждения про useLayoutEffect (Dropdown) ожидаемы и к проверке не относятся
|
|
21
|
+
// eslint-disable-next-line no-console
|
|
22
|
+
var originalError = console.error;
|
|
23
|
+
beforeAll(function () {
|
|
24
|
+
jest.spyOn(console, 'error').mockImplementation(function (message) {
|
|
25
|
+
if (typeof message === 'string' && message.includes('useLayoutEffect does nothing on the server')) return;
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
28
|
+
rest[_key - 1] = arguments[_key];
|
|
29
|
+
}
|
|
30
|
+
originalError.apply(void 0, [message].concat(rest));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
afterAll(function () {
|
|
34
|
+
return jest.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
var noop = jest.fn();
|
|
37
|
+
var renderButton = function renderButton(markup) {
|
|
38
|
+
var match = markup.match(/<button[^>]*aria-haspopup[^>]*>/);
|
|
39
|
+
if (!match) throw new Error("\u041A\u043D\u043E\u043F\u043A\u0430 \u0432\u044B\u0431\u043E\u0440\u0430 \u0441\u0442\u0440\u0430\u043D\u044B \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430: ".concat(markup));
|
|
40
|
+
return match[0];
|
|
41
|
+
};
|
|
42
|
+
describe('CountrySelect: доступное имя и состояние кнопки (A11Y-12)', function () {
|
|
43
|
+
test('имя по умолчанию — «Код страны: <страна>», свёрнут, раскрывает listbox', function () {
|
|
44
|
+
var button = renderButton((0, _server.renderToStaticMarkup)(/*#__PURE__*/_react["default"].createElement(_.CountrySelect, {
|
|
45
|
+
value: "KAZ",
|
|
46
|
+
onChange: noop,
|
|
47
|
+
options: options,
|
|
48
|
+
selectedLabel: ""
|
|
49
|
+
})));
|
|
50
|
+
expect(button).toContain('aria-label="Код страны: Казахстан"');
|
|
51
|
+
expect(button).toContain('aria-haspopup="listbox"');
|
|
52
|
+
expect(button).toContain('aria-expanded="false"');
|
|
53
|
+
expect(button).not.toContain('aria-controls');
|
|
54
|
+
});
|
|
55
|
+
test('префикс имени задаётся пропом buttonAriaLabelPrefix', function () {
|
|
56
|
+
var button = renderButton((0, _server.renderToStaticMarkup)(/*#__PURE__*/_react["default"].createElement(_.CountrySelect, {
|
|
57
|
+
value: "RUS",
|
|
58
|
+
onChange: noop,
|
|
59
|
+
options: options,
|
|
60
|
+
selectedLabel: "",
|
|
61
|
+
buttonAriaLabelPrefix: "Country code"
|
|
62
|
+
})));
|
|
63
|
+
expect(button).toContain('aria-label="Country code: Россия"');
|
|
64
|
+
});
|
|
65
|
+
test('PhoneInput пробрасывает buttonAriaLabelPrefix из countrySelectProps', function () {
|
|
66
|
+
var button = renderButton((0, _server.renderToStaticMarkup)(/*#__PURE__*/_react["default"].createElement(_PhoneInput.PhoneInput, {
|
|
67
|
+
label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D",
|
|
68
|
+
value: "",
|
|
69
|
+
onChange: noop,
|
|
70
|
+
countries: [{
|
|
71
|
+
label: 'Russia',
|
|
72
|
+
value: 'RUS'
|
|
73
|
+
}, {
|
|
74
|
+
label: 'Belarus',
|
|
75
|
+
value: 'BLR'
|
|
76
|
+
}],
|
|
77
|
+
countrySelectProps: {
|
|
78
|
+
selectedLabel: '',
|
|
79
|
+
placeholder: '',
|
|
80
|
+
buttonAriaLabelPrefix: 'Country code'
|
|
81
|
+
}
|
|
82
|
+
})));
|
|
83
|
+
expect(button).toContain('aria-label="Country code: Russia"');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -11,7 +11,7 @@ export type TProps = {
|
|
|
11
11
|
dropdownProps?: Pick<TDropdownProps, 'isAutoPosition' | 'positionLeft' | 'positionRight' | 'positionVertical'>;
|
|
12
12
|
useModernStyles?: boolean;
|
|
13
13
|
required?: boolean;
|
|
14
|
-
} & Pick<TPureInputProps, 'hasError' | 'name' | 'onBlur' | 'onFocus' | 'id'> & Pick<TInputLabelProps, 'label'> & Pick<TCalendarProps, 'maxDate' | 'minDate'> & {
|
|
14
|
+
} & Pick<TPureInputProps, 'hasError' | 'name' | 'onBlur' | 'onFocus' | 'id' | 'inputMode'> & Pick<TInputLabelProps, 'label'> & Pick<TCalendarProps, 'maxDate' | 'minDate'> & {
|
|
15
15
|
size?: TSizes;
|
|
16
16
|
};
|
|
17
17
|
export declare const DateInput: FC<TProps>;
|
|
@@ -46,12 +46,17 @@ var DateInput = exports.DateInput = function DateInput(_ref) {
|
|
|
46
46
|
useModernStyles = _ref$useModernStyles === void 0 ? false : _ref$useModernStyles,
|
|
47
47
|
_ref$required = _ref.required,
|
|
48
48
|
required = _ref$required === void 0 ? false : _ref$required,
|
|
49
|
-
id = _ref.id
|
|
49
|
+
id = _ref.id,
|
|
50
|
+
inputMode = _ref.inputMode;
|
|
50
51
|
var palette = (0, _palette.useComponentPalette)('dateInput');
|
|
51
52
|
var inputRef = (0, _react.useRef)(null);
|
|
52
53
|
var fallbackId = (0, _react.useId)();
|
|
53
54
|
var inputId = id !== null && id !== void 0 ? id : fallbackId;
|
|
54
55
|
var isMobile = (0, _index.useMobile)();
|
|
56
|
+
// На мобильном тап по полю открывает календарь в шторке — экранная клавиатура поверх него
|
|
57
|
+
// не нужна (MOB-8). Ввод с клавиатуры остаётся (физическая клавиатура, вставка); проп
|
|
58
|
+
// inputMode переопределяет значение, например 'none' для всех тач-экранов
|
|
59
|
+
var fieldInputMode = inputMode !== null && inputMode !== void 0 ? inputMode : isMobile ? 'none' : undefined;
|
|
55
60
|
var _useState = (0, _react.useState)(false),
|
|
56
61
|
_useState2 = _slicedToArray(_useState, 2),
|
|
57
62
|
isOpen = _useState2[0],
|
|
@@ -142,6 +147,7 @@ var DateInput = exports.DateInput = function DateInput(_ref) {
|
|
|
142
147
|
onBlur: handleBlur,
|
|
143
148
|
value: stringValue,
|
|
144
149
|
onChange: handleStringValueChange,
|
|
150
|
+
inputMode: fieldInputMode,
|
|
145
151
|
paddingRight: 44,
|
|
146
152
|
useModernStyles: useModernStyles
|
|
147
153
|
}, paddingAndVariantOptions)), /*#__PURE__*/_react["default"].createElement(S.Icon, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _react = _interopRequireDefault(require("react"));
|
|
4
|
+
var _server = require("react-dom/server");
|
|
5
|
+
var _ = require(".");
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
|
|
7
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
8
|
+
// Серверный рендер: предупреждения про useLayoutEffect (Dropdown) ожидаемы и к проверке не относятся
|
|
9
|
+
// eslint-disable-next-line no-console
|
|
10
|
+
var originalError = console.error;
|
|
11
|
+
beforeAll(function () {
|
|
12
|
+
jest.spyOn(console, 'error').mockImplementation(function (message) {
|
|
13
|
+
if (typeof message === 'string' && message.includes('useLayoutEffect does nothing on the server')) return;
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
16
|
+
rest[_key - 1] = arguments[_key];
|
|
17
|
+
}
|
|
18
|
+
originalError.apply(void 0, [message].concat(rest));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
afterAll(function () {
|
|
22
|
+
return jest.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
var noop = jest.fn();
|
|
25
|
+
|
|
26
|
+
// useMobile читает window.matchMedia при первом рендере
|
|
27
|
+
var withViewport = function withViewport(isMobile, render) {
|
|
28
|
+
var globalWithWindow = globalThis;
|
|
29
|
+
var previous = globalWithWindow.window;
|
|
30
|
+
globalWithWindow.window = {
|
|
31
|
+
matchMedia: function matchMedia() {
|
|
32
|
+
return {
|
|
33
|
+
matches: isMobile
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
return render();
|
|
39
|
+
} finally {
|
|
40
|
+
globalWithWindow.window = previous;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var renderInput = function renderInput(isMobile) {
|
|
44
|
+
var _markup$match$, _markup$match;
|
|
45
|
+
var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
46
|
+
var markup = withViewport(isMobile, function () {
|
|
47
|
+
return (0, _server.renderToStaticMarkup)(/*#__PURE__*/_react["default"].createElement(_.DateInput, _extends({
|
|
48
|
+
id: "date",
|
|
49
|
+
label: "\u0414\u0430\u0442\u0430",
|
|
50
|
+
value: null,
|
|
51
|
+
onChange: noop
|
|
52
|
+
}, props)));
|
|
53
|
+
});
|
|
54
|
+
return (_markup$match$ = (_markup$match = markup.match(/<input[^>]*>/)) === null || _markup$match === void 0 ? void 0 : _markup$match[0]) !== null && _markup$match$ !== void 0 ? _markup$match$ : '';
|
|
55
|
+
};
|
|
56
|
+
describe('DateInput: экранная клавиатура (MOB-8)', function () {
|
|
57
|
+
test('на мобильном — inputmode="none": тап открывает только календарь', function () {
|
|
58
|
+
expect(renderInput(true)).toContain('inputMode="none"');
|
|
59
|
+
});
|
|
60
|
+
test('на десктопе inputmode не задан', function () {
|
|
61
|
+
expect(renderInput(false)).not.toContain('inputMode');
|
|
62
|
+
});
|
|
63
|
+
test('проп inputMode переопределяет значение по умолчанию', function () {
|
|
64
|
+
expect(renderInput(false, {
|
|
65
|
+
inputMode: 'none'
|
|
66
|
+
})).toContain('inputMode="none"');
|
|
67
|
+
expect(renderInput(true, {
|
|
68
|
+
inputMode: 'numeric'
|
|
69
|
+
})).toContain('inputMode="numeric"');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -10,7 +10,7 @@ type TFieldProps = Pick<TPureInputProps, 'name' | 'onBlur' | 'onFocus'> & {
|
|
|
10
10
|
};
|
|
11
11
|
type TControlProps = Pick<TPureInputProps, 'disabled' | 'id' | 'autoFocus' | 'useModernStyles'> & {
|
|
12
12
|
label: string;
|
|
13
|
-
countrySelectProps?: Pick<TCountrySelectProps, 'selectedLabel' | 'placeholder'>;
|
|
13
|
+
countrySelectProps?: Pick<TCountrySelectProps, 'selectedLabel' | 'placeholder' | 'buttonAriaLabelPrefix'>;
|
|
14
14
|
onCountryChange?: (countryCode: string) => void;
|
|
15
15
|
countries?: Array<{
|
|
16
16
|
label: string;
|
|
@@ -212,7 +212,8 @@ var PhoneInput = exports.PhoneInput = function PhoneInput(_ref) {
|
|
|
212
212
|
positionRight: "0",
|
|
213
213
|
positionLeft: "-20px",
|
|
214
214
|
disabled: disabled,
|
|
215
|
-
selectedLabel: (countrySelectProps === null || countrySelectProps === void 0 ? void 0 : countrySelectProps.selectedLabel) || ''
|
|
215
|
+
selectedLabel: (countrySelectProps === null || countrySelectProps === void 0 ? void 0 : countrySelectProps.selectedLabel) || '',
|
|
216
|
+
buttonAriaLabelPrefix: countrySelectProps === null || countrySelectProps === void 0 ? void 0 : countrySelectProps.buttonAriaLabelPrefix
|
|
216
217
|
}))), /*#__PURE__*/_react["default"].createElement(_reactInputMask["default"], {
|
|
217
218
|
value: inputValue,
|
|
218
219
|
onFocus: focusHandler,
|
|
@@ -7,6 +7,10 @@ export type TProps = {
|
|
|
7
7
|
onChange: (value: number | null) => void;
|
|
8
8
|
max?: number;
|
|
9
9
|
upAndDown?: boolean;
|
|
10
|
+
/** Только для скринридеров и голосового управления, на экране не выводится: aria-label кнопки «вверх» */
|
|
11
|
+
incrementButtonAriaLabel?: string;
|
|
12
|
+
/** Только для скринридеров и голосового управления, на экране не выводится: aria-label кнопки «вниз» */
|
|
13
|
+
decrementButtonAriaLabel?: string;
|
|
10
14
|
} & Pick<TInputLabelProps, 'label'> & Pick<TPureInputProps, 'placeholder' | 'hasError' | 'onFocus' | 'onBlur' | 'disabled' | 'name' | 'useModernStyles' | 'id'> & {
|
|
11
15
|
size?: TSizes;
|
|
12
16
|
};
|
|
@@ -35,7 +35,13 @@ var PositiveIntegerInput = exports.PositiveIntegerInput = function PositiveInteg
|
|
|
35
35
|
size = _ref.size,
|
|
36
36
|
_ref$useModernStyles = _ref.useModernStyles,
|
|
37
37
|
useModernStyles = _ref$useModernStyles === void 0 ? false : _ref$useModernStyles,
|
|
38
|
-
id = _ref.id
|
|
38
|
+
id = _ref.id,
|
|
39
|
+
_ref$disabled = _ref.disabled,
|
|
40
|
+
disabled = _ref$disabled === void 0 ? false : _ref$disabled,
|
|
41
|
+
_ref$incrementButtonA = _ref.incrementButtonAriaLabel,
|
|
42
|
+
incrementButtonAriaLabel = _ref$incrementButtonA === void 0 ? 'Увеличить' : _ref$incrementButtonA,
|
|
43
|
+
_ref$decrementButtonA = _ref.decrementButtonAriaLabel,
|
|
44
|
+
decrementButtonAriaLabel = _ref$decrementButtonA === void 0 ? 'Уменьшить' : _ref$decrementButtonA;
|
|
39
45
|
var _useInputFocus = (0, _useInputFocus2.useInputFocus)({
|
|
40
46
|
onFocus: onFocus,
|
|
41
47
|
onBlur: onBlur
|
|
@@ -85,12 +91,32 @@ var PositiveIntegerInput = exports.PositiveIntegerInput = function PositiveInteg
|
|
|
85
91
|
var handleDown = (0, _react.useCallback)(function () {
|
|
86
92
|
onChange(decrement(value));
|
|
87
93
|
}, [decrement, value, onChange]);
|
|
94
|
+
var isUpDisabled = disabled || isIncrementDisabled;
|
|
95
|
+
var isDownDisabled = disabled || isDecrementDisabled;
|
|
96
|
+
|
|
97
|
+
// Стрелки клавиатуры в поле — как кнопки «вверх»/«вниз» (кнопки вне порядка Tab)
|
|
98
|
+
var handleKeyDown = (0, _react.useCallback)(function (event) {
|
|
99
|
+
if (!upAndDown) return;
|
|
100
|
+
if (event.key === 'ArrowUp') {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
if (!isUpDisabled) handleUp();
|
|
103
|
+
} else if (event.key === 'ArrowDown') {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
if (!isDownDisabled) handleDown();
|
|
106
|
+
}
|
|
107
|
+
}, [handleDown, handleUp, isDownDisabled, isUpDisabled, upAndDown]);
|
|
108
|
+
|
|
109
|
+
// Клик по кнопке не забирает фокус у поля: иначе срабатывал бы onBlur поля
|
|
110
|
+
var preventFocusLoss = (0, _react.useCallback)(function (event) {
|
|
111
|
+
return event.preventDefault();
|
|
112
|
+
}, []);
|
|
88
113
|
return /*#__PURE__*/_react["default"].createElement(_InputLabel.InputLabel, {
|
|
89
114
|
inFocus: inFocus,
|
|
90
115
|
label: inputLabel,
|
|
91
116
|
useModernStyles: useModernStyles,
|
|
92
117
|
size: fieldSize,
|
|
93
|
-
htmlFor: inputId
|
|
118
|
+
htmlFor: inputId,
|
|
119
|
+
disabled: disabled
|
|
94
120
|
}, /*#__PURE__*/_react["default"].createElement(S.ControlWrapper, null, modernPlaceholder, /*#__PURE__*/_react["default"].createElement(_PureInput.PureInput, _extends({
|
|
95
121
|
id: inputId,
|
|
96
122
|
"aria-label": label && !inputLabel ? label : undefined,
|
|
@@ -102,18 +128,32 @@ var PositiveIntegerInput = exports.PositiveIntegerInput = function PositiveInteg
|
|
|
102
128
|
onBlur: handleBlur,
|
|
103
129
|
value: normalizeFrom(value),
|
|
104
130
|
onChange: handleChange,
|
|
131
|
+
onKeyDown: handleKeyDown,
|
|
132
|
+
disabled: disabled,
|
|
133
|
+
inputMode: "numeric",
|
|
134
|
+
pattern: "[0-9]*",
|
|
105
135
|
paddingRight: upAndDown ? 44 : undefined
|
|
106
136
|
}, paddingAndVariantOptions, {
|
|
107
137
|
useModernStyles: useModernStyles
|
|
108
138
|
})), upAndDown && /*#__PURE__*/_react["default"].createElement(S.Arrows, null, /*#__PURE__*/_react["default"].createElement(S.Arrow, {
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
type: "button",
|
|
140
|
+
tabIndex: -1,
|
|
141
|
+
"aria-label": incrementButtonAriaLabel,
|
|
142
|
+
"aria-controls": inputId,
|
|
143
|
+
disabled: isUpDisabled,
|
|
144
|
+
onMouseDown: preventFocusLoss,
|
|
145
|
+
onClick: handleUp
|
|
111
146
|
}, /*#__PURE__*/_react["default"].createElement(_Arrow.Arrow, {
|
|
112
147
|
isOpen: true,
|
|
113
148
|
outterSize: 12
|
|
114
149
|
})), /*#__PURE__*/_react["default"].createElement(S.Arrow, {
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
type: "button",
|
|
151
|
+
tabIndex: -1,
|
|
152
|
+
"aria-label": decrementButtonAriaLabel,
|
|
153
|
+
"aria-controls": inputId,
|
|
154
|
+
disabled: isDownDisabled,
|
|
155
|
+
onMouseDown: preventFocusLoss,
|
|
156
|
+
onClick: handleDown
|
|
117
157
|
}, /*#__PURE__*/_react["default"].createElement(_Arrow.Arrow, {
|
|
118
158
|
isOpen: false,
|
|
119
159
|
outterSize: 12
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _react = _interopRequireDefault(require("react"));
|
|
4
|
+
var _server = require("react-dom/server");
|
|
5
|
+
var _ = require(".");
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
|
|
7
|
+
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
8
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
9
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
10
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
11
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
12
|
+
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
13
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
14
|
+
var noop = jest.fn();
|
|
15
|
+
var render = function render() {
|
|
16
|
+
var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
17
|
+
return (0, _server.renderToStaticMarkup)(/*#__PURE__*/_react["default"].createElement(_.PositiveIntegerInput, _extends({
|
|
18
|
+
id: "count",
|
|
19
|
+
label: "\u041A\u043E\u043B-\u0432\u043E",
|
|
20
|
+
value: 2,
|
|
21
|
+
onChange: noop,
|
|
22
|
+
max: 5,
|
|
23
|
+
upAndDown: true
|
|
24
|
+
}, props)));
|
|
25
|
+
};
|
|
26
|
+
var buttons = function buttons(markup) {
|
|
27
|
+
var _markup$match;
|
|
28
|
+
return (_markup$match = markup.match(/<button[^>]*>/g)) !== null && _markup$match !== void 0 ? _markup$match : [];
|
|
29
|
+
};
|
|
30
|
+
var input = function input(markup) {
|
|
31
|
+
var _markup$match$, _markup$match2;
|
|
32
|
+
return (_markup$match$ = (_markup$match2 = markup.match(/<input[^>]*>/)) === null || _markup$match2 === void 0 ? void 0 : _markup$match2[0]) !== null && _markup$match$ !== void 0 ? _markup$match$ : '';
|
|
33
|
+
};
|
|
34
|
+
describe('PositiveIntegerInput: стрелки «вверх/вниз» (A11Y-16)', function () {
|
|
35
|
+
test('стрелки — кнопки с именами вне порядка Tab', function () {
|
|
36
|
+
var _buttons = buttons(render()),
|
|
37
|
+
_buttons2 = _slicedToArray(_buttons, 2),
|
|
38
|
+
up = _buttons2[0],
|
|
39
|
+
down = _buttons2[1];
|
|
40
|
+
expect(up).toContain('type="button"');
|
|
41
|
+
expect(up).toContain('tabindex="-1"');
|
|
42
|
+
expect(up).toContain('aria-label="Увеличить"');
|
|
43
|
+
expect(up).toContain('aria-controls="count"');
|
|
44
|
+
expect(up).not.toContain('disabled');
|
|
45
|
+
expect(down).toContain('aria-label="Уменьшить"');
|
|
46
|
+
expect(down).not.toContain('disabled');
|
|
47
|
+
});
|
|
48
|
+
test('имена кнопок задаются пропами', function () {
|
|
49
|
+
var _buttons3 = buttons(render({
|
|
50
|
+
incrementButtonAriaLabel: 'Увеличить количество',
|
|
51
|
+
decrementButtonAriaLabel: 'Уменьшить количество'
|
|
52
|
+
})),
|
|
53
|
+
_buttons4 = _slicedToArray(_buttons3, 2),
|
|
54
|
+
up = _buttons4[0],
|
|
55
|
+
down = _buttons4[1];
|
|
56
|
+
expect(up).toContain('aria-label="Увеличить количество"');
|
|
57
|
+
expect(down).toContain('aria-label="Уменьшить количество"');
|
|
58
|
+
});
|
|
59
|
+
test('на границах недоступна соответствующая кнопка', function () {
|
|
60
|
+
expect(buttons(render({
|
|
61
|
+
value: 5
|
|
62
|
+
}))[0]).toContain('disabled');
|
|
63
|
+
expect(buttons(render({
|
|
64
|
+
value: 0
|
|
65
|
+
}))[1]).toContain('disabled');
|
|
66
|
+
});
|
|
67
|
+
test('disabled блокирует поле и обе кнопки', function () {
|
|
68
|
+
var markup = render({
|
|
69
|
+
disabled: true
|
|
70
|
+
});
|
|
71
|
+
expect(input(markup)).toContain('disabled');
|
|
72
|
+
buttons(markup).forEach(function (button) {
|
|
73
|
+
return expect(button).toContain('disabled');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
test('цифровая клавиатура на телефоне', function () {
|
|
77
|
+
var field = input(render());
|
|
78
|
+
expect(field).toContain('inputMode="numeric"');
|
|
79
|
+
expect(field).toContain('pattern="[0-9]*"');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
export declare const Arrows: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
3
|
-
export declare const Arrow: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components
|
|
4
|
-
$disabled: boolean;
|
|
5
|
-
}>> & string;
|
|
3
|
+
export declare const Arrow: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>> & string;
|
|
6
4
|
export declare const ControlWrapper: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
4
3
|
Object.defineProperty(exports, "__esModule", {
|
|
5
4
|
value: true
|
|
6
5
|
});
|
|
7
6
|
exports.ControlWrapper = exports.Arrows = exports.Arrow = void 0;
|
|
8
|
-
var _styledComponents =
|
|
9
|
-
function
|
|
10
|
-
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
|
|
7
|
+
var _styledComponents = _interopRequireDefault(require("styled-components"));
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
|
|
11
9
|
var Arrows = exports.Arrows = _styledComponents["default"].div.withConfig({
|
|
12
10
|
displayName: "styles__Arrows",
|
|
13
11
|
componentId: "sc-ctutr8-0"
|
|
14
|
-
})(["display:flex;flex-direction:column;position:absolute;height:100%;justify-content:center;top:0;right:
|
|
15
|
-
|
|
12
|
+
})(["display:flex;flex-direction:column;position:absolute;height:100%;justify-content:center;top:0;right:12px;"]);
|
|
13
|
+
|
|
14
|
+
// Кнопка 24×24 (WCAG 2.5.8) со стрелкой 12 px по центру; в низком поле кнопки сжимаются по высоте,
|
|
15
|
+
// чтобы не вылезать за его границы
|
|
16
|
+
var Arrow = exports.Arrow = _styledComponents["default"].button.withConfig({
|
|
16
17
|
displayName: "styles__Arrow",
|
|
17
18
|
componentId: "sc-ctutr8-1"
|
|
18
|
-
})(["
|
|
19
|
-
var $disabled = _ref.$disabled;
|
|
20
|
-
return $disabled && (0, _styledComponents.css)(["opacity:0.4;pointer-events:none;"]);
|
|
21
|
-
});
|
|
19
|
+
})(["flex:0 1 24px;min-height:0;width:24px;display:flex;align-items:center;justify-content:center;padding:0;border:0;background:transparent;color:inherit;cursor:pointer;user-select:none;&:disabled{opacity:0.4;cursor:default;}"]);
|
|
22
20
|
var ControlWrapper = exports.ControlWrapper = _styledComponents["default"].div.withConfig({
|
|
23
21
|
displayName: "styles__ControlWrapper",
|
|
24
22
|
componentId: "sc-ctutr8-2"
|
|
@@ -158,13 +158,20 @@ var SelectInner = function SelectInner(props) {
|
|
|
158
158
|
}, [activeIndex, baseId]);
|
|
159
159
|
var dropdownRef = (0, _react.useRef)(null);
|
|
160
160
|
var wrapperRef = (0, _useOutsideClick.useOutsideClick)(handleHide, [dropdownRef]);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
|
|
162
|
+
// Список не открывается на фокус (WCAG 2.4.11): иначе при переходе по Tab он перекрывает
|
|
163
|
+
// следующие элементы. Открытие — кликом, Enter, пробелом или стрелками
|
|
164
|
+
var handleFocus = focusHandler;
|
|
165
165
|
var handleBlur = (0, _react.useCallback)(function (event) {
|
|
166
|
+
var _wrapperRef$current, _dropdownRef$current;
|
|
166
167
|
var relatedTarget = event.relatedTarget;
|
|
167
168
|
var isOptionClick = (relatedTarget === null || relatedTarget === void 0 ? void 0 : relatedTarget.dataset.optionIdentifier) === OPTION_IDENTIFIER;
|
|
169
|
+
|
|
170
|
+
// Фокус ушёл на другой элемент вне поля и списка — закрываем список. Без relatedTarget
|
|
171
|
+
// (клик по неактивной области списка, уход из окна) не закрываем: клик снаружи
|
|
172
|
+
// обрабатывает useOutsideClick
|
|
173
|
+
var isFocusOutside = !!relatedTarget && !((_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.contains(relatedTarget)) && !((_dropdownRef$current = dropdownRef.current) !== null && _dropdownRef$current !== void 0 && _dropdownRef$current.contains(relatedTarget));
|
|
174
|
+
if (isFocusOutside) handleHide();
|
|
168
175
|
if (!isOptionClick) {
|
|
169
176
|
blurHandler(event);
|
|
170
177
|
return;
|
|
@@ -172,7 +179,7 @@ var SelectInner = function SelectInner(props) {
|
|
|
172
179
|
delayBlur.current = function () {
|
|
173
180
|
return blurHandler(event);
|
|
174
181
|
};
|
|
175
|
-
}, [blurHandler]);
|
|
182
|
+
}, [blurHandler, handleHide, wrapperRef]);
|
|
176
183
|
var selectValue = (0, _react.useCallback)(function (newValue) {
|
|
177
184
|
if (isMultiple) {
|
|
178
185
|
var next = multipleValue.includes(newValue) ? multipleValue.filter(function (item) {
|
|
@@ -224,6 +231,10 @@ var SelectInner = function SelectInner(props) {
|
|
|
224
231
|
handleHide();
|
|
225
232
|
}
|
|
226
233
|
break;
|
|
234
|
+
case 'Tab':
|
|
235
|
+
// фокус уходит дальше по странице — список не должен оставаться поверх
|
|
236
|
+
if (isOpen) handleHide();
|
|
237
|
+
break;
|
|
227
238
|
default:
|
|
228
239
|
break;
|
|
229
240
|
}
|
|
@@ -241,6 +252,12 @@ var SelectInner = function SelectInner(props) {
|
|
|
241
252
|
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.blur();
|
|
242
253
|
handleShow();
|
|
243
254
|
};
|
|
255
|
+
|
|
256
|
+
// Раньше список открывался фокусом, который ставит и клик; теперь клик открывает его сам.
|
|
257
|
+
// Повторный клик по полю закрывает список, как у нативного select
|
|
258
|
+
var handleToggleClick = function handleToggleClick() {
|
|
259
|
+
if (isOpen) handleHide();else handleShow();
|
|
260
|
+
};
|
|
244
261
|
var handleOptionRender = function handleOptionRender(option) {
|
|
245
262
|
if (renderOption) {
|
|
246
263
|
if (!option.entity) {
|
|
@@ -367,7 +384,7 @@ var SelectInner = function SelectInner(props) {
|
|
|
367
384
|
isOpen: isOpen,
|
|
368
385
|
onReset: isResetButtonEnabled ? handleReset : undefined,
|
|
369
386
|
size: fieldSize,
|
|
370
|
-
onClick: isDrawerOptions || !!renderedValue ? handleInputClick :
|
|
387
|
+
onClick: isDrawerOptions || !!renderedValue ? handleInputClick : handleToggleClick,
|
|
371
388
|
required: required,
|
|
372
389
|
useModernStyles: useModernStyles,
|
|
373
390
|
comboboxProps: {
|
|
@@ -18,7 +18,9 @@ export var CountryItem = function CountryItem(_ref) {
|
|
|
18
18
|
return /*#__PURE__*/React.createElement(S.Wrapper, {
|
|
19
19
|
$palette: palette,
|
|
20
20
|
onClick: handleSelect,
|
|
21
|
-
type: "button"
|
|
21
|
+
type: "button",
|
|
22
|
+
role: "option",
|
|
23
|
+
"aria-selected": isSelected
|
|
22
24
|
}, /*#__PURE__*/React.createElement(S.Title, {
|
|
23
25
|
$isSelected: isSelected,
|
|
24
26
|
$palette: palette
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var _excluded = ["value", "onChange", "options", "placeholder", "disabled", "selectedLabel"];
|
|
1
|
+
var _excluded = ["value", "onChange", "options", "placeholder", "disabled", "selectedLabel", "buttonAriaLabelPrefix"];
|
|
2
2
|
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
3
3
|
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
|
|
4
4
|
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
@@ -12,7 +12,7 @@ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" !=
|
|
|
12
12
|
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
13
13
|
function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var s = Object.getOwnPropertySymbols(e); for (r = 0; r < s.length; r++) o = s[r], t.includes(o) || {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
|
|
14
14
|
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (e.includes(n)) continue; t[n] = r[n]; } return t; }
|
|
15
|
-
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
15
|
+
import React, { useCallback, useId, useMemo, useRef, useState } from 'react';
|
|
16
16
|
import * as S from './stlyes';
|
|
17
17
|
import { CountryItem } from './components/CountryItem';
|
|
18
18
|
import { SearchInput } from '../SearchInput';
|
|
@@ -31,7 +31,10 @@ export var CountrySelect = function CountrySelect(_ref) {
|
|
|
31
31
|
_ref$disabled = _ref.disabled,
|
|
32
32
|
disabled = _ref$disabled === void 0 ? false : _ref$disabled,
|
|
33
33
|
selectedLabel = _ref.selectedLabel,
|
|
34
|
+
_ref$buttonAriaLabelP = _ref.buttonAriaLabelPrefix,
|
|
35
|
+
buttonAriaLabelPrefix = _ref$buttonAriaLabelP === void 0 ? 'Код страны' : _ref$buttonAriaLabelP,
|
|
34
36
|
dropdownPositionProps = _objectWithoutProperties(_ref, _excluded);
|
|
37
|
+
var listboxId = useId();
|
|
35
38
|
var palette = useComponentPalette('countrySelect');
|
|
36
39
|
var _useState = useState(false),
|
|
37
40
|
_useState2 = _slicedToArray(_useState, 2),
|
|
@@ -78,8 +81,12 @@ export var CountrySelect = function CountrySelect(_ref) {
|
|
|
78
81
|
}, /*#__PURE__*/React.createElement(S.HeaderWrapper, {
|
|
79
82
|
type: "button",
|
|
80
83
|
onClick: handleDropdownTrigger,
|
|
81
|
-
disabled: disabled
|
|
82
|
-
|
|
84
|
+
disabled: disabled,
|
|
85
|
+
"aria-label": selectedOption ? "".concat(buttonAriaLabelPrefix, ": ").concat(selectedOption.label) : buttonAriaLabelPrefix,
|
|
86
|
+
"aria-haspopup": "listbox",
|
|
87
|
+
"aria-expanded": isOpen,
|
|
88
|
+
"aria-controls": isOpen ? listboxId : undefined
|
|
89
|
+
}, (selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.flag) && /*#__PURE__*/React.createElement(Flag, {
|
|
83
90
|
isSmall: false,
|
|
84
91
|
flag: selectedOption.flag
|
|
85
92
|
}), !disabled && /*#__PURE__*/React.createElement(Arrow, {
|
|
@@ -99,6 +106,10 @@ export var CountrySelect = function CountrySelect(_ref) {
|
|
|
99
106
|
value: search
|
|
100
107
|
})), /*#__PURE__*/React.createElement(Scrollbar, {
|
|
101
108
|
maxHeight: 192
|
|
109
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
110
|
+
role: "listbox",
|
|
111
|
+
id: listboxId,
|
|
112
|
+
"aria-label": buttonAriaLabelPrefix
|
|
102
113
|
}, filtredOptions.map(function (option) {
|
|
103
114
|
return /*#__PURE__*/React.createElement(CountryItem, {
|
|
104
115
|
key: option.value,
|
|
@@ -112,5 +123,5 @@ export var CountrySelect = function CountrySelect(_ref) {
|
|
|
112
123
|
hint: option.hint,
|
|
113
124
|
flag: option.flag
|
|
114
125
|
});
|
|
115
|
-
})))));
|
|
126
|
+
}))))));
|
|
116
127
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import { CountrySelect } from '.';
|
|
4
|
+
import { PhoneInput } from '../PhoneInput';
|
|
5
|
+
var options = [{
|
|
6
|
+
label: 'Россия',
|
|
7
|
+
value: 'RUS',
|
|
8
|
+
description: '+7',
|
|
9
|
+
flag: 'RUS'
|
|
10
|
+
}, {
|
|
11
|
+
label: 'Казахстан',
|
|
12
|
+
value: 'KAZ',
|
|
13
|
+
description: '+7',
|
|
14
|
+
flag: 'KAZ'
|
|
15
|
+
}];
|
|
16
|
+
|
|
17
|
+
// Серверный рендер: предупреждения про useLayoutEffect (Dropdown) ожидаемы и к проверке не относятся
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
var originalError = console.error;
|
|
20
|
+
beforeAll(function () {
|
|
21
|
+
jest.spyOn(console, 'error').mockImplementation(function (message) {
|
|
22
|
+
if (typeof message === 'string' && message.includes('useLayoutEffect does nothing on the server')) return;
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
25
|
+
rest[_key - 1] = arguments[_key];
|
|
26
|
+
}
|
|
27
|
+
originalError.apply(void 0, [message].concat(rest));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
afterAll(function () {
|
|
31
|
+
return jest.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
var noop = jest.fn();
|
|
34
|
+
var renderButton = function renderButton(markup) {
|
|
35
|
+
var match = markup.match(/<button[^>]*aria-haspopup[^>]*>/);
|
|
36
|
+
if (!match) throw new Error("\u041A\u043D\u043E\u043F\u043A\u0430 \u0432\u044B\u0431\u043E\u0440\u0430 \u0441\u0442\u0440\u0430\u043D\u044B \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430: ".concat(markup));
|
|
37
|
+
return match[0];
|
|
38
|
+
};
|
|
39
|
+
describe('CountrySelect: доступное имя и состояние кнопки (A11Y-12)', function () {
|
|
40
|
+
test('имя по умолчанию — «Код страны: <страна>», свёрнут, раскрывает listbox', function () {
|
|
41
|
+
var button = renderButton(renderToStaticMarkup(/*#__PURE__*/React.createElement(CountrySelect, {
|
|
42
|
+
value: "KAZ",
|
|
43
|
+
onChange: noop,
|
|
44
|
+
options: options,
|
|
45
|
+
selectedLabel: ""
|
|
46
|
+
})));
|
|
47
|
+
expect(button).toContain('aria-label="Код страны: Казахстан"');
|
|
48
|
+
expect(button).toContain('aria-haspopup="listbox"');
|
|
49
|
+
expect(button).toContain('aria-expanded="false"');
|
|
50
|
+
expect(button).not.toContain('aria-controls');
|
|
51
|
+
});
|
|
52
|
+
test('префикс имени задаётся пропом buttonAriaLabelPrefix', function () {
|
|
53
|
+
var button = renderButton(renderToStaticMarkup(/*#__PURE__*/React.createElement(CountrySelect, {
|
|
54
|
+
value: "RUS",
|
|
55
|
+
onChange: noop,
|
|
56
|
+
options: options,
|
|
57
|
+
selectedLabel: "",
|
|
58
|
+
buttonAriaLabelPrefix: "Country code"
|
|
59
|
+
})));
|
|
60
|
+
expect(button).toContain('aria-label="Country code: Россия"');
|
|
61
|
+
});
|
|
62
|
+
test('PhoneInput пробрасывает buttonAriaLabelPrefix из countrySelectProps', function () {
|
|
63
|
+
var button = renderButton(renderToStaticMarkup(/*#__PURE__*/React.createElement(PhoneInput, {
|
|
64
|
+
label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D",
|
|
65
|
+
value: "",
|
|
66
|
+
onChange: noop,
|
|
67
|
+
countries: [{
|
|
68
|
+
label: 'Russia',
|
|
69
|
+
value: 'RUS'
|
|
70
|
+
}, {
|
|
71
|
+
label: 'Belarus',
|
|
72
|
+
value: 'BLR'
|
|
73
|
+
}],
|
|
74
|
+
countrySelectProps: {
|
|
75
|
+
selectedLabel: '',
|
|
76
|
+
placeholder: '',
|
|
77
|
+
buttonAriaLabelPrefix: 'Country code'
|
|
78
|
+
}
|
|
79
|
+
})));
|
|
80
|
+
expect(button).toContain('aria-label="Country code: Russia"');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -37,12 +37,17 @@ export var DateInput = function DateInput(_ref) {
|
|
|
37
37
|
useModernStyles = _ref$useModernStyles === void 0 ? false : _ref$useModernStyles,
|
|
38
38
|
_ref$required = _ref.required,
|
|
39
39
|
required = _ref$required === void 0 ? false : _ref$required,
|
|
40
|
-
id = _ref.id
|
|
40
|
+
id = _ref.id,
|
|
41
|
+
inputMode = _ref.inputMode;
|
|
41
42
|
var palette = useComponentPalette('dateInput');
|
|
42
43
|
var inputRef = useRef(null);
|
|
43
44
|
var fallbackId = useId();
|
|
44
45
|
var inputId = id !== null && id !== void 0 ? id : fallbackId;
|
|
45
46
|
var isMobile = useMobile();
|
|
47
|
+
// На мобильном тап по полю открывает календарь в шторке — экранная клавиатура поверх него
|
|
48
|
+
// не нужна (MOB-8). Ввод с клавиатуры остаётся (физическая клавиатура, вставка); проп
|
|
49
|
+
// inputMode переопределяет значение, например 'none' для всех тач-экранов
|
|
50
|
+
var fieldInputMode = inputMode !== null && inputMode !== void 0 ? inputMode : isMobile ? 'none' : undefined;
|
|
46
51
|
var _useState = useState(false),
|
|
47
52
|
_useState2 = _slicedToArray(_useState, 2),
|
|
48
53
|
isOpen = _useState2[0],
|
|
@@ -133,6 +138,7 @@ export var DateInput = function DateInput(_ref) {
|
|
|
133
138
|
onBlur: handleBlur,
|
|
134
139
|
value: stringValue,
|
|
135
140
|
onChange: handleStringValueChange,
|
|
141
|
+
inputMode: fieldInputMode,
|
|
136
142
|
paddingRight: 44,
|
|
137
143
|
useModernStyles: useModernStyles
|
|
138
144
|
}, paddingAndVariantOptions)), /*#__PURE__*/React.createElement(S.Icon, {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
4
|
+
import { DateInput } from '.';
|
|
5
|
+
|
|
6
|
+
// Серверный рендер: предупреждения про useLayoutEffect (Dropdown) ожидаемы и к проверке не относятся
|
|
7
|
+
// eslint-disable-next-line no-console
|
|
8
|
+
var originalError = console.error;
|
|
9
|
+
beforeAll(function () {
|
|
10
|
+
jest.spyOn(console, 'error').mockImplementation(function (message) {
|
|
11
|
+
if (typeof message === 'string' && message.includes('useLayoutEffect does nothing on the server')) return;
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
14
|
+
rest[_key - 1] = arguments[_key];
|
|
15
|
+
}
|
|
16
|
+
originalError.apply(void 0, [message].concat(rest));
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
afterAll(function () {
|
|
20
|
+
return jest.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
var noop = jest.fn();
|
|
23
|
+
|
|
24
|
+
// useMobile читает window.matchMedia при первом рендере
|
|
25
|
+
var withViewport = function withViewport(isMobile, render) {
|
|
26
|
+
var globalWithWindow = globalThis;
|
|
27
|
+
var previous = globalWithWindow.window;
|
|
28
|
+
globalWithWindow.window = {
|
|
29
|
+
matchMedia: function matchMedia() {
|
|
30
|
+
return {
|
|
31
|
+
matches: isMobile
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
return render();
|
|
37
|
+
} finally {
|
|
38
|
+
globalWithWindow.window = previous;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var renderInput = function renderInput(isMobile) {
|
|
42
|
+
var _markup$match$, _markup$match;
|
|
43
|
+
var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
44
|
+
var markup = withViewport(isMobile, function () {
|
|
45
|
+
return renderToStaticMarkup(/*#__PURE__*/React.createElement(DateInput, _extends({
|
|
46
|
+
id: "date",
|
|
47
|
+
label: "\u0414\u0430\u0442\u0430",
|
|
48
|
+
value: null,
|
|
49
|
+
onChange: noop
|
|
50
|
+
}, props)));
|
|
51
|
+
});
|
|
52
|
+
return (_markup$match$ = (_markup$match = markup.match(/<input[^>]*>/)) === null || _markup$match === void 0 ? void 0 : _markup$match[0]) !== null && _markup$match$ !== void 0 ? _markup$match$ : '';
|
|
53
|
+
};
|
|
54
|
+
describe('DateInput: экранная клавиатура (MOB-8)', function () {
|
|
55
|
+
test('на мобильном — inputmode="none": тап открывает только календарь', function () {
|
|
56
|
+
expect(renderInput(true)).toContain('inputMode="none"');
|
|
57
|
+
});
|
|
58
|
+
test('на десктопе inputmode не задан', function () {
|
|
59
|
+
expect(renderInput(false)).not.toContain('inputMode');
|
|
60
|
+
});
|
|
61
|
+
test('проп inputMode переопределяет значение по умолчанию', function () {
|
|
62
|
+
expect(renderInput(false, {
|
|
63
|
+
inputMode: 'none'
|
|
64
|
+
})).toContain('inputMode="none"');
|
|
65
|
+
expect(renderInput(true, {
|
|
66
|
+
inputMode: 'numeric'
|
|
67
|
+
})).toContain('inputMode="numeric"');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -202,7 +202,8 @@ export var PhoneInput = function PhoneInput(_ref) {
|
|
|
202
202
|
positionRight: "0",
|
|
203
203
|
positionLeft: "-20px",
|
|
204
204
|
disabled: disabled,
|
|
205
|
-
selectedLabel: (countrySelectProps === null || countrySelectProps === void 0 ? void 0 : countrySelectProps.selectedLabel) || ''
|
|
205
|
+
selectedLabel: (countrySelectProps === null || countrySelectProps === void 0 ? void 0 : countrySelectProps.selectedLabel) || '',
|
|
206
|
+
buttonAriaLabelPrefix: countrySelectProps === null || countrySelectProps === void 0 ? void 0 : countrySelectProps.buttonAriaLabelPrefix
|
|
206
207
|
}))), /*#__PURE__*/React.createElement(InputMask, {
|
|
207
208
|
value: inputValue,
|
|
208
209
|
onFocus: focusHandler,
|
|
@@ -26,7 +26,13 @@ export var PositiveIntegerInput = function PositiveIntegerInput(_ref) {
|
|
|
26
26
|
size = _ref.size,
|
|
27
27
|
_ref$useModernStyles = _ref.useModernStyles,
|
|
28
28
|
useModernStyles = _ref$useModernStyles === void 0 ? false : _ref$useModernStyles,
|
|
29
|
-
id = _ref.id
|
|
29
|
+
id = _ref.id,
|
|
30
|
+
_ref$disabled = _ref.disabled,
|
|
31
|
+
disabled = _ref$disabled === void 0 ? false : _ref$disabled,
|
|
32
|
+
_ref$incrementButtonA = _ref.incrementButtonAriaLabel,
|
|
33
|
+
incrementButtonAriaLabel = _ref$incrementButtonA === void 0 ? 'Увеличить' : _ref$incrementButtonA,
|
|
34
|
+
_ref$decrementButtonA = _ref.decrementButtonAriaLabel,
|
|
35
|
+
decrementButtonAriaLabel = _ref$decrementButtonA === void 0 ? 'Уменьшить' : _ref$decrementButtonA;
|
|
30
36
|
var _useInputFocus = useInputFocus({
|
|
31
37
|
onFocus: onFocus,
|
|
32
38
|
onBlur: onBlur
|
|
@@ -76,12 +82,32 @@ export var PositiveIntegerInput = function PositiveIntegerInput(_ref) {
|
|
|
76
82
|
var handleDown = useCallback(function () {
|
|
77
83
|
onChange(decrement(value));
|
|
78
84
|
}, [decrement, value, onChange]);
|
|
85
|
+
var isUpDisabled = disabled || isIncrementDisabled;
|
|
86
|
+
var isDownDisabled = disabled || isDecrementDisabled;
|
|
87
|
+
|
|
88
|
+
// Стрелки клавиатуры в поле — как кнопки «вверх»/«вниз» (кнопки вне порядка Tab)
|
|
89
|
+
var handleKeyDown = useCallback(function (event) {
|
|
90
|
+
if (!upAndDown) return;
|
|
91
|
+
if (event.key === 'ArrowUp') {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
if (!isUpDisabled) handleUp();
|
|
94
|
+
} else if (event.key === 'ArrowDown') {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
if (!isDownDisabled) handleDown();
|
|
97
|
+
}
|
|
98
|
+
}, [handleDown, handleUp, isDownDisabled, isUpDisabled, upAndDown]);
|
|
99
|
+
|
|
100
|
+
// Клик по кнопке не забирает фокус у поля: иначе срабатывал бы onBlur поля
|
|
101
|
+
var preventFocusLoss = useCallback(function (event) {
|
|
102
|
+
return event.preventDefault();
|
|
103
|
+
}, []);
|
|
79
104
|
return /*#__PURE__*/React.createElement(InputLabel, {
|
|
80
105
|
inFocus: inFocus,
|
|
81
106
|
label: inputLabel,
|
|
82
107
|
useModernStyles: useModernStyles,
|
|
83
108
|
size: fieldSize,
|
|
84
|
-
htmlFor: inputId
|
|
109
|
+
htmlFor: inputId,
|
|
110
|
+
disabled: disabled
|
|
85
111
|
}, /*#__PURE__*/React.createElement(S.ControlWrapper, null, modernPlaceholder, /*#__PURE__*/React.createElement(PureInput, _extends({
|
|
86
112
|
id: inputId,
|
|
87
113
|
"aria-label": label && !inputLabel ? label : undefined,
|
|
@@ -93,18 +119,32 @@ export var PositiveIntegerInput = function PositiveIntegerInput(_ref) {
|
|
|
93
119
|
onBlur: handleBlur,
|
|
94
120
|
value: normalizeFrom(value),
|
|
95
121
|
onChange: handleChange,
|
|
122
|
+
onKeyDown: handleKeyDown,
|
|
123
|
+
disabled: disabled,
|
|
124
|
+
inputMode: "numeric",
|
|
125
|
+
pattern: "[0-9]*",
|
|
96
126
|
paddingRight: upAndDown ? 44 : undefined
|
|
97
127
|
}, paddingAndVariantOptions, {
|
|
98
128
|
useModernStyles: useModernStyles
|
|
99
129
|
})), upAndDown && /*#__PURE__*/React.createElement(S.Arrows, null, /*#__PURE__*/React.createElement(S.Arrow, {
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
type: "button",
|
|
131
|
+
tabIndex: -1,
|
|
132
|
+
"aria-label": incrementButtonAriaLabel,
|
|
133
|
+
"aria-controls": inputId,
|
|
134
|
+
disabled: isUpDisabled,
|
|
135
|
+
onMouseDown: preventFocusLoss,
|
|
136
|
+
onClick: handleUp
|
|
102
137
|
}, /*#__PURE__*/React.createElement(Arrow, {
|
|
103
138
|
isOpen: true,
|
|
104
139
|
outterSize: 12
|
|
105
140
|
})), /*#__PURE__*/React.createElement(S.Arrow, {
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
type: "button",
|
|
142
|
+
tabIndex: -1,
|
|
143
|
+
"aria-label": decrementButtonAriaLabel,
|
|
144
|
+
"aria-controls": inputId,
|
|
145
|
+
disabled: isDownDisabled,
|
|
146
|
+
onMouseDown: preventFocusLoss,
|
|
147
|
+
onClick: handleDown
|
|
108
148
|
}, /*#__PURE__*/React.createElement(Arrow, {
|
|
109
149
|
isOpen: false,
|
|
110
150
|
outterSize: 12
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
2
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
3
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
4
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
5
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
6
|
+
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
7
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
10
|
+
import { PositiveIntegerInput } from '.';
|
|
11
|
+
var noop = jest.fn();
|
|
12
|
+
var render = function render() {
|
|
13
|
+
var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
14
|
+
return renderToStaticMarkup(/*#__PURE__*/React.createElement(PositiveIntegerInput, _extends({
|
|
15
|
+
id: "count",
|
|
16
|
+
label: "\u041A\u043E\u043B-\u0432\u043E",
|
|
17
|
+
value: 2,
|
|
18
|
+
onChange: noop,
|
|
19
|
+
max: 5,
|
|
20
|
+
upAndDown: true
|
|
21
|
+
}, props)));
|
|
22
|
+
};
|
|
23
|
+
var buttons = function buttons(markup) {
|
|
24
|
+
var _markup$match;
|
|
25
|
+
return (_markup$match = markup.match(/<button[^>]*>/g)) !== null && _markup$match !== void 0 ? _markup$match : [];
|
|
26
|
+
};
|
|
27
|
+
var input = function input(markup) {
|
|
28
|
+
var _markup$match$, _markup$match2;
|
|
29
|
+
return (_markup$match$ = (_markup$match2 = markup.match(/<input[^>]*>/)) === null || _markup$match2 === void 0 ? void 0 : _markup$match2[0]) !== null && _markup$match$ !== void 0 ? _markup$match$ : '';
|
|
30
|
+
};
|
|
31
|
+
describe('PositiveIntegerInput: стрелки «вверх/вниз» (A11Y-16)', function () {
|
|
32
|
+
test('стрелки — кнопки с именами вне порядка Tab', function () {
|
|
33
|
+
var _buttons = buttons(render()),
|
|
34
|
+
_buttons2 = _slicedToArray(_buttons, 2),
|
|
35
|
+
up = _buttons2[0],
|
|
36
|
+
down = _buttons2[1];
|
|
37
|
+
expect(up).toContain('type="button"');
|
|
38
|
+
expect(up).toContain('tabindex="-1"');
|
|
39
|
+
expect(up).toContain('aria-label="Увеличить"');
|
|
40
|
+
expect(up).toContain('aria-controls="count"');
|
|
41
|
+
expect(up).not.toContain('disabled');
|
|
42
|
+
expect(down).toContain('aria-label="Уменьшить"');
|
|
43
|
+
expect(down).not.toContain('disabled');
|
|
44
|
+
});
|
|
45
|
+
test('имена кнопок задаются пропами', function () {
|
|
46
|
+
var _buttons3 = buttons(render({
|
|
47
|
+
incrementButtonAriaLabel: 'Увеличить количество',
|
|
48
|
+
decrementButtonAriaLabel: 'Уменьшить количество'
|
|
49
|
+
})),
|
|
50
|
+
_buttons4 = _slicedToArray(_buttons3, 2),
|
|
51
|
+
up = _buttons4[0],
|
|
52
|
+
down = _buttons4[1];
|
|
53
|
+
expect(up).toContain('aria-label="Увеличить количество"');
|
|
54
|
+
expect(down).toContain('aria-label="Уменьшить количество"');
|
|
55
|
+
});
|
|
56
|
+
test('на границах недоступна соответствующая кнопка', function () {
|
|
57
|
+
expect(buttons(render({
|
|
58
|
+
value: 5
|
|
59
|
+
}))[0]).toContain('disabled');
|
|
60
|
+
expect(buttons(render({
|
|
61
|
+
value: 0
|
|
62
|
+
}))[1]).toContain('disabled');
|
|
63
|
+
});
|
|
64
|
+
test('disabled блокирует поле и обе кнопки', function () {
|
|
65
|
+
var markup = render({
|
|
66
|
+
disabled: true
|
|
67
|
+
});
|
|
68
|
+
expect(input(markup)).toContain('disabled');
|
|
69
|
+
buttons(markup).forEach(function (button) {
|
|
70
|
+
return expect(button).toContain('disabled');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
test('цифровая клавиатура на телефоне', function () {
|
|
74
|
+
var field = input(render());
|
|
75
|
+
expect(field).toContain('inputMode="numeric"');
|
|
76
|
+
expect(field).toContain('pattern="[0-9]*"');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import styled
|
|
1
|
+
import styled from 'styled-components';
|
|
2
2
|
export var Arrows = styled.div.withConfig({
|
|
3
3
|
displayName: "styles__Arrows",
|
|
4
4
|
componentId: "sc-ctutr8-0"
|
|
5
|
-
})(["display:flex;flex-direction:column;position:absolute;height:100%;justify-content:center;top:0;right:
|
|
6
|
-
|
|
5
|
+
})(["display:flex;flex-direction:column;position:absolute;height:100%;justify-content:center;top:0;right:12px;"]);
|
|
6
|
+
|
|
7
|
+
// Кнопка 24×24 (WCAG 2.5.8) со стрелкой 12 px по центру; в низком поле кнопки сжимаются по высоте,
|
|
8
|
+
// чтобы не вылезать за его границы
|
|
9
|
+
export var Arrow = styled.button.withConfig({
|
|
7
10
|
displayName: "styles__Arrow",
|
|
8
11
|
componentId: "sc-ctutr8-1"
|
|
9
|
-
})(["
|
|
10
|
-
var $disabled = _ref.$disabled;
|
|
11
|
-
return $disabled && css(["opacity:0.4;pointer-events:none;"]);
|
|
12
|
-
});
|
|
12
|
+
})(["flex:0 1 24px;min-height:0;width:24px;display:flex;align-items:center;justify-content:center;padding:0;border:0;background:transparent;color:inherit;cursor:pointer;user-select:none;&:disabled{opacity:0.4;cursor:default;}"]);
|
|
13
13
|
export var ControlWrapper = styled.div.withConfig({
|
|
14
14
|
displayName: "styles__ControlWrapper",
|
|
15
15
|
componentId: "sc-ctutr8-2"
|
|
@@ -148,13 +148,20 @@ var SelectInner = function SelectInner(props) {
|
|
|
148
148
|
}, [activeIndex, baseId]);
|
|
149
149
|
var dropdownRef = useRef(null);
|
|
150
150
|
var wrapperRef = useOutsideClick(handleHide, [dropdownRef]);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
|
|
152
|
+
// Список не открывается на фокус (WCAG 2.4.11): иначе при переходе по Tab он перекрывает
|
|
153
|
+
// следующие элементы. Открытие — кликом, Enter, пробелом или стрелками
|
|
154
|
+
var handleFocus = focusHandler;
|
|
155
155
|
var handleBlur = useCallback(function (event) {
|
|
156
|
+
var _wrapperRef$current, _dropdownRef$current;
|
|
156
157
|
var relatedTarget = event.relatedTarget;
|
|
157
158
|
var isOptionClick = (relatedTarget === null || relatedTarget === void 0 ? void 0 : relatedTarget.dataset.optionIdentifier) === OPTION_IDENTIFIER;
|
|
159
|
+
|
|
160
|
+
// Фокус ушёл на другой элемент вне поля и списка — закрываем список. Без relatedTarget
|
|
161
|
+
// (клик по неактивной области списка, уход из окна) не закрываем: клик снаружи
|
|
162
|
+
// обрабатывает useOutsideClick
|
|
163
|
+
var isFocusOutside = !!relatedTarget && !((_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.contains(relatedTarget)) && !((_dropdownRef$current = dropdownRef.current) !== null && _dropdownRef$current !== void 0 && _dropdownRef$current.contains(relatedTarget));
|
|
164
|
+
if (isFocusOutside) handleHide();
|
|
158
165
|
if (!isOptionClick) {
|
|
159
166
|
blurHandler(event);
|
|
160
167
|
return;
|
|
@@ -162,7 +169,7 @@ var SelectInner = function SelectInner(props) {
|
|
|
162
169
|
delayBlur.current = function () {
|
|
163
170
|
return blurHandler(event);
|
|
164
171
|
};
|
|
165
|
-
}, [blurHandler]);
|
|
172
|
+
}, [blurHandler, handleHide, wrapperRef]);
|
|
166
173
|
var selectValue = useCallback(function (newValue) {
|
|
167
174
|
if (isMultiple) {
|
|
168
175
|
var next = multipleValue.includes(newValue) ? multipleValue.filter(function (item) {
|
|
@@ -214,6 +221,10 @@ var SelectInner = function SelectInner(props) {
|
|
|
214
221
|
handleHide();
|
|
215
222
|
}
|
|
216
223
|
break;
|
|
224
|
+
case 'Tab':
|
|
225
|
+
// фокус уходит дальше по странице — список не должен оставаться поверх
|
|
226
|
+
if (isOpen) handleHide();
|
|
227
|
+
break;
|
|
217
228
|
default:
|
|
218
229
|
break;
|
|
219
230
|
}
|
|
@@ -231,6 +242,12 @@ var SelectInner = function SelectInner(props) {
|
|
|
231
242
|
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.blur();
|
|
232
243
|
handleShow();
|
|
233
244
|
};
|
|
245
|
+
|
|
246
|
+
// Раньше список открывался фокусом, который ставит и клик; теперь клик открывает его сам.
|
|
247
|
+
// Повторный клик по полю закрывает список, как у нативного select
|
|
248
|
+
var handleToggleClick = function handleToggleClick() {
|
|
249
|
+
if (isOpen) handleHide();else handleShow();
|
|
250
|
+
};
|
|
234
251
|
var handleOptionRender = function handleOptionRender(option) {
|
|
235
252
|
if (renderOption) {
|
|
236
253
|
if (!option.entity) {
|
|
@@ -357,7 +374,7 @@ var SelectInner = function SelectInner(props) {
|
|
|
357
374
|
isOpen: isOpen,
|
|
358
375
|
onReset: isResetButtonEnabled ? handleReset : undefined,
|
|
359
376
|
size: fieldSize,
|
|
360
|
-
onClick: isDrawerOptions || !!renderedValue ? handleInputClick :
|
|
377
|
+
onClick: isDrawerOptions || !!renderedValue ? handleInputClick : handleToggleClick,
|
|
361
378
|
required: required,
|
|
362
379
|
useModernStyles: useModernStyles,
|
|
363
380
|
comboboxProps: {
|