cozy-ui-plus 4.5.0 → 4.6.0

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.
Files changed (28) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/Contacts/AddModal/ContactForm/TextFieldPhone/ButtonBase.js +30 -0
  3. package/dist/Contacts/AddModal/ContactForm/TextFieldPhone/CustomInput.js +25 -0
  4. package/dist/Contacts/AddModal/ContactForm/TextFieldPhone/FlagBottomSheet.js +79 -0
  5. package/dist/Contacts/AddModal/ContactForm/TextFieldPhone/FlagMenu.js +76 -0
  6. package/dist/Contacts/AddModal/ContactForm/TextFieldPhone/index.js +69 -0
  7. package/dist/Contacts/AddModal/ContactForm/fieldsConfig.js +3 -0
  8. package/dist/Contacts/AddModal/mocks.js +3 -3
  9. package/dist/ContactsList/Contacts/ContactName.js +11 -15
  10. package/dist/src/Contacts/AddModal/ContactForm/TextFieldPhone/ButtonBase.d.ts +17 -0
  11. package/dist/src/Contacts/AddModal/ContactForm/TextFieldPhone/CustomInput.d.ts +2 -0
  12. package/dist/src/Contacts/AddModal/ContactForm/TextFieldPhone/FlagBottomSheet.d.ts +5 -0
  13. package/dist/src/Contacts/AddModal/ContactForm/TextFieldPhone/FlagMenu.d.ts +5 -0
  14. package/dist/src/Contacts/AddModal/ContactForm/TextFieldPhone/index.d.ts +6 -0
  15. package/dist/src/ContactsList/Contacts/ContactName.spec.d.ts +1 -0
  16. package/jest.config.js +1 -1
  17. package/package.json +3 -4
  18. package/src/Contacts/AddModal/ContactForm/TextFieldPhone/ButtonBase.jsx +19 -0
  19. package/src/Contacts/AddModal/ContactForm/TextFieldPhone/CustomInput.jsx +15 -0
  20. package/src/Contacts/AddModal/ContactForm/TextFieldPhone/FlagBottomSheet.jsx +60 -0
  21. package/src/Contacts/AddModal/ContactForm/TextFieldPhone/FlagMenu.jsx +58 -0
  22. package/src/Contacts/AddModal/ContactForm/TextFieldPhone/index.jsx +42 -0
  23. package/src/Contacts/AddModal/ContactForm/fieldsConfig.jsx +3 -0
  24. package/src/Contacts/AddModal/ContactForm/formValuesToContact.spec.js +1 -1
  25. package/src/Contacts/AddModal/ContactForm/index.spec.jsx +0 -16
  26. package/src/Contacts/AddModal/mocks.js +3 -3
  27. package/src/ContactsList/Contacts/ContactName.jsx +14 -19
  28. package/src/ContactsList/Contacts/ContactName.spec.jsx +99 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [4.6.0](https://github.com/cozy/cozy-libs/compare/cozy-ui-plus@4.5.1...cozy-ui-plus@4.6.0) (2026-02-19)
7
+
8
+ ### Features
9
+
10
+ - **cozy-ui-plus:** Add menu to select phone number by country ([7750965](https://github.com/cozy/cozy-libs/commit/775096555b17dd2464b5fcad653d58ae8993d0a4))
11
+ - **cozy-ui-plus:** Add react-international-phone package ([c6eaa2e](https://github.com/cozy/cozy-libs/commit/c6eaa2ebf6218cc81ef09c4a415c3ec0d6137436))
12
+ - **cozy-ui-plus:** Add simple small component ([aabfed6](https://github.com/cozy/cozy-libs/commit/aabfed68b92d8ba31868c4e7eebc4ad6043b57b2))
13
+ - **cozy-ui-plus:** Use new textfield for phone number ([6b47fc5](https://github.com/cozy/cozy-libs/commit/6b47fc5b742b9108f35cd0e7164e942669e93c6c))
14
+
15
+ ## [4.5.1](https://github.com/cozy/cozy-libs/compare/cozy-ui-plus@4.5.0...cozy-ui-plus@4.5.1) (2026-02-03)
16
+
17
+ ### Bug Fixes
18
+
19
+ - **cozy-ui-plus:** Fix ContactName not bolding multi-word family names ([e8015c0](https://github.com/cozy/cozy-libs/commit/e8015c05bda2272b85bf96cb1d560251097fef07))
20
+
6
21
  # [4.5.0](https://github.com/cozy/cozy-libs/compare/cozy-ui-plus@4.4.1...cozy-ui-plus@4.5.0) (2026-02-02)
7
22
 
8
23
  ### Features
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _ButtonBase = _interopRequireDefault(require("cozy-ui/transpiled/react/ButtonBase"));
11
+
12
+ var _styles = require("cozy-ui/transpiled/react/styles");
13
+
14
+ var styles = function styles(theme) {
15
+ return {
16
+ root: {
17
+ '&:hover': {
18
+ textDecoration: 'none',
19
+ backgroundColor: (0, _styles.alpha)(theme.palette.text.primary, theme.palette.action.hoverOpacity),
20
+ '@media (hover: none)': {
21
+ backgroundColor: 'transparent'
22
+ }
23
+ }
24
+ }
25
+ };
26
+ };
27
+
28
+ var _default = (0, _styles.withStyles)(styles)(_ButtonBase.default);
29
+
30
+ exports.default = _default;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
11
+
12
+ var _react = _interopRequireDefault(require("react"));
13
+
14
+ var _Input = _interopRequireDefault(require("cozy-ui/transpiled/react/Input"));
15
+
16
+ var _ButtonBase = _interopRequireDefault(require("./ButtonBase"));
17
+
18
+ var CustomInput = function CustomInput(props) {
19
+ return /*#__PURE__*/_react.default.createElement(_ButtonBase.default, null, /*#__PURE__*/_react.default.createElement(_Input.default, (0, _extends2.default)({}, props, {
20
+ disableUnderline: true
21
+ })));
22
+ };
23
+
24
+ var _default = CustomInput;
25
+ exports.default = _default;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
11
+
12
+ var _react = _interopRequireWildcard(require("react"));
13
+
14
+ require("react-international-phone/style.css");
15
+
16
+ var _BottomSheet = _interopRequireWildcard(require("cozy-ui/transpiled/react/BottomSheet"));
17
+
18
+ var _ListItemIcon = _interopRequireDefault(require("cozy-ui/transpiled/react/ListItemIcon"));
19
+
20
+ var _ListItemText = _interopRequireDefault(require("cozy-ui/transpiled/react/ListItemText"));
21
+
22
+ var _MenuItem = _interopRequireDefault(require("cozy-ui/transpiled/react/MenuItem"));
23
+
24
+ var _Typography = _interopRequireDefault(require("cozy-ui/transpiled/react/Typography"));
25
+
26
+ var _ButtonBase = _interopRequireDefault(require("./ButtonBase"));
27
+
28
+ var _reactInternationalPhone = require("react-international-phone");
29
+
30
+ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
31
+
32
+ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
33
+
34
+ var FlagBottomSheet = function FlagBottomSheet(_ref) {
35
+ var value = _ref.value,
36
+ setCountry = _ref.setCountry;
37
+
38
+ var _useState = (0, _react.useState)(false),
39
+ _useState2 = (0, _slicedToArray2.default)(_useState, 2),
40
+ open = _useState2[0],
41
+ setOpen = _useState2[1];
42
+
43
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_ButtonBase.default, {
44
+ onClick: function onClick() {
45
+ return setOpen(true);
46
+ }
47
+ }, /*#__PURE__*/_react.default.createElement(_reactInternationalPhone.FlagImage, {
48
+ iso2: value
49
+ })), open && /*#__PURE__*/_react.default.createElement(_BottomSheet.default, {
50
+ backdrop: true,
51
+ onClose: function onClose() {
52
+ return setOpen(false);
53
+ }
54
+ }, /*#__PURE__*/_react.default.createElement(_BottomSheet.BottomSheetItem, null, _reactInternationalPhone.defaultCountries.map(function (c) {
55
+ var _parseCountry = (0, _reactInternationalPhone.parseCountry)(c),
56
+ iso2 = _parseCountry.iso2,
57
+ name = _parseCountry.name,
58
+ dialCode = _parseCountry.dialCode;
59
+
60
+ return /*#__PURE__*/_react.default.createElement(_MenuItem.default, {
61
+ key: iso2,
62
+ value: iso2,
63
+ onClick: function onClick() {
64
+ setCountry(iso2);
65
+ setOpen(false);
66
+ }
67
+ }, /*#__PURE__*/_react.default.createElement(_ListItemIcon.default, null, /*#__PURE__*/_react.default.createElement(_reactInternationalPhone.FlagImage, {
68
+ iso2: iso2
69
+ })), /*#__PURE__*/_react.default.createElement(_ListItemText.default, {
70
+ primary: name
71
+ }), /*#__PURE__*/_react.default.createElement(_Typography.default, {
72
+ className: "u-mr-half",
73
+ align: "right"
74
+ }, "+", dialCode));
75
+ }))));
76
+ };
77
+
78
+ var _default = FlagBottomSheet;
79
+ exports.default = _default;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _react = _interopRequireDefault(require("react"));
11
+
12
+ require("react-international-phone/style.css");
13
+
14
+ var _ListItemIcon = _interopRequireDefault(require("cozy-ui/transpiled/react/ListItemIcon"));
15
+
16
+ var _ListItemText = _interopRequireDefault(require("cozy-ui/transpiled/react/ListItemText"));
17
+
18
+ var _MenuItem = _interopRequireDefault(require("cozy-ui/transpiled/react/MenuItem"));
19
+
20
+ var _Select = _interopRequireDefault(require("cozy-ui/transpiled/react/Select"));
21
+
22
+ var _Typography = _interopRequireDefault(require("cozy-ui/transpiled/react/Typography"));
23
+
24
+ var _CustomInput = _interopRequireDefault(require("./CustomInput"));
25
+
26
+ var _reactInternationalPhone = require("react-international-phone");
27
+
28
+ var FlagMenu = function FlagMenu(_ref) {
29
+ var value = _ref.value,
30
+ setCountry = _ref.setCountry;
31
+ return /*#__PURE__*/_react.default.createElement(_Select.default, {
32
+ input: /*#__PURE__*/_react.default.createElement(_CustomInput.default, null),
33
+ MenuProps: {
34
+ style: {
35
+ height: '300px'
36
+ },
37
+ transformOrigin: {
38
+ vertical: 'top',
39
+ horizontal: 'left'
40
+ }
41
+ },
42
+ value: value,
43
+ renderValue: function renderValue(v) {
44
+ return /*#__PURE__*/_react.default.createElement(_reactInternationalPhone.FlagImage, {
45
+ iso2: v,
46
+ style: {
47
+ verticalAlign: 'bottom',
48
+ marginLeft: '0.5rem'
49
+ }
50
+ });
51
+ },
52
+ onChange: function onChange(e) {
53
+ return setCountry(e.target.value);
54
+ }
55
+ }, _reactInternationalPhone.defaultCountries.map(function (c) {
56
+ var _parseCountry = (0, _reactInternationalPhone.parseCountry)(c),
57
+ iso2 = _parseCountry.iso2,
58
+ name = _parseCountry.name,
59
+ dialCode = _parseCountry.dialCode;
60
+
61
+ return /*#__PURE__*/_react.default.createElement(_MenuItem.default, {
62
+ key: iso2,
63
+ value: iso2
64
+ }, /*#__PURE__*/_react.default.createElement(_ListItemIcon.default, null, /*#__PURE__*/_react.default.createElement(_reactInternationalPhone.FlagImage, {
65
+ iso2: iso2
66
+ })), /*#__PURE__*/_react.default.createElement(_ListItemText.default, {
67
+ primary: name
68
+ }), /*#__PURE__*/_react.default.createElement(_Typography.default, {
69
+ className: "u-mr-half",
70
+ align: "right"
71
+ }, "+", dialCode));
72
+ }));
73
+ };
74
+
75
+ var _default = FlagMenu;
76
+ exports.default = _default;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.default = void 0;
9
+
10
+ var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
11
+
12
+ var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
13
+
14
+ var _react = _interopRequireDefault(require("react"));
15
+
16
+ var _reactInternationalPhone = require("react-international-phone");
17
+
18
+ var _InputAdornment = _interopRequireDefault(require("cozy-ui/transpiled/react/InputAdornment"));
19
+
20
+ var _TextField = _interopRequireDefault(require("cozy-ui/transpiled/react/TextField"));
21
+
22
+ var _Breakpoints = require("cozy-ui/transpiled/react/providers/Breakpoints");
23
+
24
+ var _FlagBottomSheet = _interopRequireDefault(require("./FlagBottomSheet"));
25
+
26
+ var _FlagMenu = _interopRequireDefault(require("./FlagMenu"));
27
+
28
+ var _excluded = ["value", "onChange"];
29
+
30
+ var TextFieldPhone = function TextFieldPhone(_ref) {
31
+ var value = _ref.value,
32
+ _onChange = _ref.onChange,
33
+ props = (0, _objectWithoutProperties2.default)(_ref, _excluded);
34
+
35
+ var _useBreakpoints = (0, _Breakpoints.useBreakpoints)(),
36
+ isMobile = _useBreakpoints.isMobile;
37
+
38
+ var _usePhoneInput = (0, _reactInternationalPhone.usePhoneInput)({
39
+ defaultCountry: 'fr',
40
+ value: value,
41
+ countries: _reactInternationalPhone.defaultCountries,
42
+ onChange: function onChange(data) {
43
+ _onChange(data.phone);
44
+ }
45
+ }),
46
+ inputValue = _usePhoneInput.inputValue,
47
+ handlePhoneValueChange = _usePhoneInput.handlePhoneValueChange,
48
+ inputRef = _usePhoneInput.inputRef,
49
+ country = _usePhoneInput.country,
50
+ setCountry = _usePhoneInput.setCountry;
51
+
52
+ var ResponsiveFlagMenu = isMobile ? _FlagBottomSheet.default : _FlagMenu.default;
53
+ return /*#__PURE__*/_react.default.createElement(_TextField.default, (0, _extends2.default)({
54
+ value: inputValue,
55
+ inputRef: inputRef,
56
+ InputProps: {
57
+ startAdornment: /*#__PURE__*/_react.default.createElement(_InputAdornment.default, {
58
+ position: "start"
59
+ }, /*#__PURE__*/_react.default.createElement(ResponsiveFlagMenu, {
60
+ value: country.iso2,
61
+ setCountry: setCountry
62
+ }))
63
+ },
64
+ onChange: handlePhoneValueChange
65
+ }, props));
66
+ };
67
+
68
+ var _default = TextFieldPhone;
69
+ exports.default = _default;
@@ -35,6 +35,8 @@ var _Telephone = _interopRequireDefault(require("cozy-ui/transpiled/react/Icons/
35
35
 
36
36
  var _InputAdornment = _interopRequireDefault(require("cozy-ui/transpiled/react/InputAdornment"));
37
37
 
38
+ var _index = _interopRequireDefault(require("./TextFieldPhone/index"));
39
+
38
40
  /**
39
41
  * @type {import('../types').Field[]}
40
42
  */
@@ -82,6 +84,7 @@ var fields = [{
82
84
  icon: _Telephone.default,
83
85
  type: 'tel',
84
86
  layout: 'array',
87
+ component: _index.default,
85
88
  label: {
86
89
  name: 'phoneLabel',
87
90
  select: true,
@@ -72,7 +72,7 @@ var johnDoeContact = {
72
72
  },
73
73
  note: 'Atque cupiditate saepe omnis quos ut molestiae labore voluptates omnis.',
74
74
  phone: [{
75
- number: '+33 (2)0 90 00 54 04',
75
+ number: '+33 2 90 00 54 04',
76
76
  primary: true
77
77
  }, {
78
78
  number: '+33 6 77 11 22 33',
@@ -137,7 +137,7 @@ var johnDoeFormValues = {
137
137
  }],
138
138
  phone: [{
139
139
  fieldId: 'fieldId_5',
140
- phone: '+33 (2)0 90 00 54 04',
140
+ phone: '+33 2 90 00 54 04',
141
141
  phoneLabel: undefined
142
142
  }, {
143
143
  fieldId: 'fieldId_6',
@@ -219,7 +219,7 @@ var johnDoeContactWithRelated = {
219
219
  },
220
220
  note: 'Atque cupiditate saepe omnis quos ut molestiae labore voluptates omnis.',
221
221
  phone: [{
222
- number: '+33 (2)0 90 00 54 04',
222
+ number: '+33 2 90 00 54 04',
223
223
  primary: true
224
224
  }, {
225
225
  number: '+33 6 77 11 22 33',
@@ -7,8 +7,6 @@ Object.defineProperty(exports, "__esModule", {
7
7
  });
8
8
  exports.default = void 0;
9
9
 
10
- var _classnames = _interopRequireDefault(require("classnames"));
11
-
12
10
  var _propTypes = _interopRequireDefault(require("prop-types"));
13
11
 
14
12
  var _react = _interopRequireDefault(require("react"));
@@ -18,13 +16,17 @@ var _contact = require("cozy-client/dist/models/contact");
18
16
  var _Typography = _interopRequireDefault(require("cozy-ui/transpiled/react/Typography"));
19
17
 
20
18
  var ContactName = function ContactName(_ref) {
21
- var _contact$name;
19
+ var _getDisplayName;
22
20
 
23
21
  var contact = _ref.contact,
24
22
  disable = _ref.disable;
25
- var familyName = (_contact$name = contact.name) === null || _contact$name === void 0 ? void 0 : _contact$name.familyName;
26
- var displayName = (0, _contact.getDisplayName)(contact);
27
- var namesToDisplay = displayName === null || displayName === void 0 ? void 0 : displayName.split(' ');
23
+
24
+ var _ref2 = contact.name || {},
25
+ familyName = _ref2.familyName;
26
+
27
+ var displayName = (_getDisplayName = (0, _contact.getDisplayName)(contact)) === null || _getDisplayName === void 0 ? void 0 : _getDisplayName.trim();
28
+ var hasFamilyName = familyName && (displayName === null || displayName === void 0 ? void 0 : displayName.endsWith(familyName));
29
+ var namePrefix = hasFamilyName ? displayName.slice(0, -familyName.length).trimEnd() : null;
28
30
  return /*#__PURE__*/_react.default.createElement(_Typography.default, {
29
31
  "data-testid": "ContactName" // used by a test in cozy-contacts
30
32
  ,
@@ -32,15 +34,9 @@ var ContactName = function ContactName(_ref) {
32
34
  noWrap: true,
33
35
  display: "inline",
34
36
  color: disable ? 'textSecondary' : 'textPrimary'
35
- }, namesToDisplay === null || namesToDisplay === void 0 ? void 0 : namesToDisplay.map(function (name, index) {
36
- var isLastItem = index === namesToDisplay.length - 1;
37
- return /*#__PURE__*/_react.default.createElement("span", {
38
- key: "display-".concat(index),
39
- className: (0, _classnames.default)({
40
- 'u-fw-bold': name === familyName
41
- })
42
- }, name, !isLastItem && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, "\xA0"));
43
- }));
37
+ }, hasFamilyName ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, namePrefix && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, namePrefix, "\xA0"), /*#__PURE__*/_react.default.createElement("span", {
38
+ className: "u-fw-bold"
39
+ }, familyName)) : displayName);
44
40
  };
45
41
 
46
42
  ContactName.propTypes = {
@@ -0,0 +1,17 @@
1
+ declare const _default: import("react").ComponentType<Pick<{
2
+ action?: import("react").Ref<import("@material-ui/core").ButtonBaseActions>;
3
+ buttonRef?: import("react").Ref<unknown>;
4
+ centerRipple?: boolean;
5
+ children?: import("react").ReactNode;
6
+ disabled?: boolean;
7
+ disableRipple?: boolean;
8
+ disableTouchRipple?: boolean;
9
+ focusRipple?: boolean;
10
+ focusVisibleClassName?: string;
11
+ onFocusVisible?: import("react").FocusEventHandler<any>;
12
+ tabIndex?: string | number;
13
+ TouchRippleProps?: Partial<import("@material-ui/core/ButtonBase/TouchRipple").TouchRippleProps>;
14
+ } & import("@material-ui/core/OverridableComponent").CommonProps<import("@material-ui/core").ExtendButtonBaseTypeMap<import("@material-ui/core").ButtonBaseTypeMap<{}, "button">>> & Pick<Pick<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "key" | keyof import("react").ButtonHTMLAttributes<HTMLButtonElement>> & {
15
+ ref?: import("react").Ref<HTMLButtonElement>;
16
+ }, "name" | "color" | "hidden" | "ref" | "form" | "title" | "key" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "id" | "lang" | "placeholder" | "slot" | "spellCheck" | "inputMode" | "is" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "type" | "value" | "autoFocus" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget">, "name" | "color" | "hidden" | "style" | "disabled" | "ref" | "form" | "title" | "children" | "className" | "key" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "id" | "lang" | "placeholder" | "slot" | "spellCheck" | "tabIndex" | "inputMode" | "is" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "type" | "value" | "innerRef" | "autoFocus" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget" | "action" | "buttonRef" | "centerRipple" | "disableRipple" | "disableTouchRipple" | "focusRipple" | "focusVisibleClassName" | "onFocusVisible" | "TouchRippleProps"> & import("@material-ui/styles/withStyles/withStyles").StyledComponentProps<"root">>;
17
+ export default _default;
@@ -0,0 +1,2 @@
1
+ export default CustomInput;
2
+ declare function CustomInput(props: any): JSX.Element;
@@ -0,0 +1,5 @@
1
+ export default FlagBottomSheet;
2
+ declare function FlagBottomSheet({ value, setCountry }: {
3
+ value: any;
4
+ setCountry: any;
5
+ }): JSX.Element;
@@ -0,0 +1,5 @@
1
+ export default FlagMenu;
2
+ declare function FlagMenu({ value, setCountry }: {
3
+ value: any;
4
+ setCountry: any;
5
+ }): JSX.Element;
@@ -0,0 +1,6 @@
1
+ export default TextFieldPhone;
2
+ declare function TextFieldPhone({ value, onChange, ...props }: {
3
+ [x: string]: any;
4
+ value: any;
5
+ onChange: any;
6
+ }): JSX.Element;
package/jest.config.js CHANGED
@@ -5,7 +5,7 @@ module.exports = {
5
5
  moduleDirectories: ['src', 'node_modules'],
6
6
  moduleNameMapper: {
7
7
  '\\.(png|gif|jpe?g|svg)$': '<rootDir>/test/fileMock.js',
8
- '\\.styl$': 'identity-obj-proxy',
8
+ '\\.(styl|css)$': 'identity-obj-proxy',
9
9
  '^cozy-client$': '<rootDir>/node_modules/cozy-client/dist/index',
10
10
  '^cozy-client/dist/types$':
11
11
  '<rootDir>/node_modules/cozy-client/dist/types.js',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-ui-plus",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
4
4
  "description": "Cozy-ui-plus is an extension of cozy-ui and provides components based on cozy-client",
5
5
  "main": "dist/index.js",
6
6
  "license": "MIT",
@@ -44,8 +44,6 @@
44
44
  "node-polyglot": "^2.4.0",
45
45
  "prop-types": "15.8.1",
46
46
  "react": "16.12.0",
47
- "react-dnd": "16.0.1",
48
- "react-dnd-html5-backend": "16.0.1",
49
47
  "react-dom": "16.12.0",
50
48
  "react-test-renderer": "16.12.0",
51
49
  "stylus": "0.64.0",
@@ -62,6 +60,7 @@
62
60
  "mime-types": "2.1.35",
63
61
  "react-final-form": "6.5.9",
64
62
  "react-final-form-arrays": "3.1.4",
63
+ "react-international-phone": "4.7.0",
65
64
  "rooks": "7.14.1"
66
65
  },
67
66
  "peerDependencies": {
@@ -74,5 +73,5 @@
74
73
  "react-dom": "^16 || ^17 || ^18",
75
74
  "twake-i18n": ">=0.3.0"
76
75
  },
77
- "gitHead": "61e7d0678b71327a91c8b527edda7e06994ab43d"
76
+ "gitHead": "8a5f3cb0c64efb15a79cb856927dba5513a94623"
78
77
  }
@@ -0,0 +1,19 @@
1
+ import ButtonBase from 'cozy-ui/transpiled/react/ButtonBase'
2
+ import { alpha, withStyles } from 'cozy-ui/transpiled/react/styles'
3
+
4
+ const styles = theme => ({
5
+ root: {
6
+ '&:hover': {
7
+ textDecoration: 'none',
8
+ backgroundColor: alpha(
9
+ theme.palette.text.primary,
10
+ theme.palette.action.hoverOpacity
11
+ ),
12
+ '@media (hover: none)': {
13
+ backgroundColor: 'transparent'
14
+ }
15
+ }
16
+ }
17
+ })
18
+
19
+ export default withStyles(styles)(ButtonBase)
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+
3
+ import Input from 'cozy-ui/transpiled/react/Input'
4
+
5
+ import ButtonBase from './ButtonBase'
6
+
7
+ const CustomInput = props => {
8
+ return (
9
+ <ButtonBase>
10
+ <Input {...props} disableUnderline />
11
+ </ButtonBase>
12
+ )
13
+ }
14
+
15
+ export default CustomInput
@@ -0,0 +1,60 @@
1
+ import React, { useState } from 'react'
2
+ import 'react-international-phone/style.css'
3
+
4
+ import BottomSheet, {
5
+ BottomSheetItem
6
+ } from 'cozy-ui/transpiled/react/BottomSheet'
7
+ import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
8
+ import ListItemText from 'cozy-ui/transpiled/react/ListItemText'
9
+ import MenuItem from 'cozy-ui/transpiled/react/MenuItem'
10
+ import Typography from 'cozy-ui/transpiled/react/Typography'
11
+
12
+ import ButtonBase from './ButtonBase'
13
+
14
+ import {
15
+ defaultCountries,
16
+ FlagImage,
17
+ parseCountry
18
+ } from 'react-international-phone'
19
+
20
+ const FlagBottomSheet = ({ value, setCountry }) => {
21
+ const [open, setOpen] = useState(false)
22
+
23
+ return (
24
+ <>
25
+ <ButtonBase onClick={() => setOpen(true)}>
26
+ <FlagImage iso2={value} />
27
+ </ButtonBase>
28
+
29
+ {open && (
30
+ <BottomSheet backdrop onClose={() => setOpen(false)}>
31
+ <BottomSheetItem>
32
+ {defaultCountries.map(c => {
33
+ const { iso2, name, dialCode } = parseCountry(c)
34
+ return (
35
+ <MenuItem
36
+ key={iso2}
37
+ value={iso2}
38
+ onClick={() => {
39
+ setCountry(iso2)
40
+ setOpen(false)
41
+ }}
42
+ >
43
+ <ListItemIcon>
44
+ <FlagImage iso2={iso2} />
45
+ </ListItemIcon>
46
+ <ListItemText primary={name} />
47
+ <Typography className="u-mr-half" align="right">
48
+ +{dialCode}
49
+ </Typography>
50
+ </MenuItem>
51
+ )
52
+ })}
53
+ </BottomSheetItem>
54
+ </BottomSheet>
55
+ )}
56
+ </>
57
+ )
58
+ }
59
+
60
+ export default FlagBottomSheet
@@ -0,0 +1,58 @@
1
+ import React from 'react'
2
+ import 'react-international-phone/style.css'
3
+
4
+ import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
5
+ import ListItemText from 'cozy-ui/transpiled/react/ListItemText'
6
+ import MenuItem from 'cozy-ui/transpiled/react/MenuItem'
7
+ import Select from 'cozy-ui/transpiled/react/Select'
8
+ import Typography from 'cozy-ui/transpiled/react/Typography'
9
+
10
+ import CustomInput from './CustomInput'
11
+
12
+ import {
13
+ defaultCountries,
14
+ FlagImage,
15
+ parseCountry
16
+ } from 'react-international-phone'
17
+
18
+ const FlagMenu = ({ value, setCountry }) => {
19
+ return (
20
+ <Select
21
+ input={<CustomInput />}
22
+ MenuProps={{
23
+ style: {
24
+ height: '300px'
25
+ },
26
+ transformOrigin: {
27
+ vertical: 'top',
28
+ horizontal: 'left'
29
+ }
30
+ }}
31
+ value={value}
32
+ renderValue={v => (
33
+ <FlagImage
34
+ iso2={v}
35
+ style={{ verticalAlign: 'bottom', marginLeft: '0.5rem' }}
36
+ />
37
+ )}
38
+ onChange={e => setCountry(e.target.value)}
39
+ >
40
+ {defaultCountries.map(c => {
41
+ const { iso2, name, dialCode } = parseCountry(c)
42
+ return (
43
+ <MenuItem key={iso2} value={iso2}>
44
+ <ListItemIcon>
45
+ <FlagImage iso2={iso2} />
46
+ </ListItemIcon>
47
+ <ListItemText primary={name} />
48
+ <Typography className="u-mr-half" align="right">
49
+ +{dialCode}
50
+ </Typography>
51
+ </MenuItem>
52
+ )
53
+ })}
54
+ </Select>
55
+ )
56
+ }
57
+
58
+ export default FlagMenu
@@ -0,0 +1,42 @@
1
+ import React from 'react'
2
+ import { defaultCountries, usePhoneInput } from 'react-international-phone'
3
+
4
+ import InputAdornment from 'cozy-ui/transpiled/react/InputAdornment'
5
+ import TextField from 'cozy-ui/transpiled/react/TextField'
6
+ import { useBreakpoints } from 'cozy-ui/transpiled/react/providers/Breakpoints'
7
+
8
+ import FlagBottomSheet from './FlagBottomSheet'
9
+ import FlagMenu from './FlagMenu'
10
+
11
+ const TextFieldPhone = ({ value, onChange, ...props }) => {
12
+ const { isMobile } = useBreakpoints()
13
+ const { inputValue, handlePhoneValueChange, inputRef, country, setCountry } =
14
+ usePhoneInput({
15
+ defaultCountry: 'fr',
16
+ value,
17
+ countries: defaultCountries,
18
+ onChange: data => {
19
+ onChange(data.phone)
20
+ }
21
+ })
22
+
23
+ const ResponsiveFlagMenu = isMobile ? FlagBottomSheet : FlagMenu
24
+
25
+ return (
26
+ <TextField
27
+ value={inputValue}
28
+ inputRef={inputRef}
29
+ InputProps={{
30
+ startAdornment: (
31
+ <InputAdornment position="start">
32
+ <ResponsiveFlagMenu value={country.iso2} setCountry={setCountry} />
33
+ </InputAdornment>
34
+ )
35
+ }}
36
+ onChange={handlePhoneValueChange}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ export default TextFieldPhone
@@ -14,6 +14,8 @@ import RelationshipIcon from 'cozy-ui/transpiled/react/Icons/Relationship'
14
14
  import TelephoneIcon from 'cozy-ui/transpiled/react/Icons/Telephone'
15
15
  import InputAdornment from 'cozy-ui/transpiled/react/InputAdornment'
16
16
 
17
+ import TextFieldPhone from './TextFieldPhone/index'
18
+
17
19
  /**
18
20
  * @type {import('../types').Field[]}
19
21
  */
@@ -74,6 +76,7 @@ export const fields = [
74
76
  icon: TelephoneIcon,
75
77
  type: 'tel',
76
78
  layout: 'array',
79
+ component: TextFieldPhone,
77
80
  label: {
78
81
  name: 'phoneLabel',
79
82
  select: true,
@@ -107,7 +107,7 @@ describe('formValuesToContact', () => {
107
107
  note: 'Atque cupiditate saepe omnis quos ut molestiae labore voluptates omnis.',
108
108
  phone: [
109
109
  {
110
- number: '+33 (2)0 90 00 54 04',
110
+ number: '+33 2 90 00 54 04',
111
111
  primary: true,
112
112
  type: undefined,
113
113
  label: undefined
@@ -211,22 +211,6 @@ describe('ContactForm', () => {
211
211
  expect(screen.queryByDisplayValue(testFields['Notes'])).not.toBeNull()
212
212
  })
213
213
 
214
- it('should not submit empty fields', () => {
215
- const onSubmit = jest.fn()
216
-
217
- render(
218
- <DemoProvider>
219
- <ContactForm onSubmit={onSubmit} onCancel={() => {}} />
220
- </DemoProvider>
221
- )
222
-
223
- act(() => {
224
- fireEvent.submit(screen.getByRole('form'))
225
- })
226
-
227
- expect(onSubmit).not.toBeCalled()
228
- })
229
-
230
214
  describe('isSameContactProp', () => {
231
215
  it('should return true if contacts have the same "related" relationships', () => {
232
216
  const prevProps = {
@@ -80,7 +80,7 @@ export const johnDoeContact = {
80
80
  note: 'Atque cupiditate saepe omnis quos ut molestiae labore voluptates omnis.',
81
81
  phone: [
82
82
  {
83
- number: '+33 (2)0 90 00 54 04',
83
+ number: '+33 2 90 00 54 04',
84
84
  primary: true
85
85
  },
86
86
  {
@@ -155,7 +155,7 @@ export const johnDoeFormValues = {
155
155
  phone: [
156
156
  {
157
157
  fieldId: 'fieldId_5',
158
- phone: '+33 (2)0 90 00 54 04',
158
+ phone: '+33 2 90 00 54 04',
159
159
  phoneLabel: undefined
160
160
  },
161
161
  {
@@ -254,7 +254,7 @@ export const johnDoeContactWithRelated = {
254
254
  note: 'Atque cupiditate saepe omnis quos ut molestiae labore voluptates omnis.',
255
255
  phone: [
256
256
  {
257
- number: '+33 (2)0 90 00 54 04',
257
+ number: '+33 2 90 00 54 04',
258
258
  primary: true
259
259
  },
260
260
  {
@@ -1,4 +1,3 @@
1
- import cx from 'classnames'
2
1
  import PropTypes from 'prop-types'
3
2
  import React from 'react'
4
3
 
@@ -6,9 +5,12 @@ import { getDisplayName } from 'cozy-client/dist/models/contact'
6
5
  import Typography from 'cozy-ui/transpiled/react/Typography'
7
6
 
8
7
  const ContactName = ({ contact, disable }) => {
9
- const familyName = contact.name?.familyName
10
- const displayName = getDisplayName(contact)
11
- const namesToDisplay = displayName?.split(' ')
8
+ const { familyName } = contact.name || {}
9
+ const displayName = getDisplayName(contact)?.trim()
10
+ const hasFamilyName = familyName && displayName?.endsWith(familyName)
11
+ const namePrefix = hasFamilyName
12
+ ? displayName.slice(0, -familyName.length).trimEnd()
13
+ : null
12
14
 
13
15
  return (
14
16
  <Typography
@@ -18,21 +20,14 @@ const ContactName = ({ contact, disable }) => {
18
20
  display="inline"
19
21
  color={disable ? 'textSecondary' : 'textPrimary'}
20
22
  >
21
- {namesToDisplay?.map((name, index) => {
22
- const isLastItem = index === namesToDisplay.length - 1
23
-
24
- return (
25
- <span
26
- key={`display-${index}`}
27
- className={cx({
28
- 'u-fw-bold': name === familyName
29
- })}
30
- >
31
- {name}
32
- {!isLastItem && <>&nbsp;</>}
33
- </span>
34
- )
35
- })}
23
+ {hasFamilyName ? (
24
+ <>
25
+ {namePrefix && <>{namePrefix}&nbsp;</>}
26
+ <span className="u-fw-bold">{familyName}</span>
27
+ </>
28
+ ) : (
29
+ displayName
30
+ )}
36
31
  </Typography>
37
32
  )
38
33
  }
@@ -0,0 +1,99 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import React from 'react'
3
+
4
+ import { getDisplayName } from 'cozy-client/dist/models/contact'
5
+
6
+ import ContactName from './ContactName'
7
+
8
+ jest.mock('cozy-client/dist/models/contact', () => ({
9
+ getDisplayName: jest.fn()
10
+ }))
11
+
12
+ const makeContact = attrs => ({
13
+ _id: 'contactid',
14
+ _type: 'io.cozy.contacts',
15
+ ...attrs
16
+ })
17
+
18
+ describe('ContactName', () => {
19
+ it('should bold single-word family name', () => {
20
+ getDisplayName.mockReturnValue('John Doe')
21
+ const contact = makeContact({
22
+ name: { familyName: 'Doe', givenName: 'John' }
23
+ })
24
+
25
+ render(<ContactName contact={contact} />)
26
+
27
+ const boldElement = document.querySelector('.u-fw-bold')
28
+ expect(boldElement).toHaveTextContent('Doe')
29
+ })
30
+
31
+ it('should bold multi-word family name', () => {
32
+ getDisplayName.mockReturnValue('khaled Van Beethoven')
33
+ const contact = makeContact({
34
+ name: { familyName: 'Van Beethoven', givenName: 'khaled' }
35
+ })
36
+
37
+ render(<ContactName contact={contact} />)
38
+
39
+ const boldElement = document.querySelector('.u-fw-bold')
40
+ expect(boldElement).toHaveTextContent('Van Beethoven')
41
+ })
42
+
43
+ it('should not bold anything when familyName is missing', () => {
44
+ getDisplayName.mockReturnValue('johndoe@localhost')
45
+ const contact = makeContact()
46
+
47
+ render(<ContactName contact={contact} />)
48
+
49
+ const boldElement = document.querySelector('.u-fw-bold')
50
+ expect(boldElement).toBeNull()
51
+ expect(screen.getByText('johndoe@localhost')).toBeInTheDocument()
52
+ })
53
+
54
+ it('should not bold when displayName does not end with familyName', () => {
55
+ getDisplayName.mockReturnValue('johndoe@localhost')
56
+ const contact = makeContact({
57
+ name: { familyName: 'Doe' }
58
+ })
59
+
60
+ render(<ContactName contact={contact} />)
61
+
62
+ const boldElement = document.querySelector('.u-fw-bold')
63
+ expect(boldElement).toBeNull()
64
+ })
65
+
66
+ it('should handle contact without name property', () => {
67
+ getDisplayName.mockReturnValue('johndoe@localhost')
68
+ const contact = makeContact()
69
+
70
+ render(<ContactName contact={contact} />)
71
+
72
+ expect(screen.getByText('johndoe@localhost')).toBeInTheDocument()
73
+ })
74
+
75
+ it('should display name prefix before bolded family name', () => {
76
+ getDisplayName.mockReturnValue('khaled Van Beethoven')
77
+ const contact = makeContact({
78
+ name: { familyName: 'Van Beethoven', givenName: 'khaled' }
79
+ })
80
+
81
+ render(<ContactName contact={contact} />)
82
+
83
+ const nameElement = screen.getByTestId('ContactName')
84
+ expect(nameElement).toHaveTextContent('khaled')
85
+ expect(nameElement).toHaveTextContent('Van Beethoven')
86
+ })
87
+
88
+ it('should bold family name even with trailing whitespace in displayName', () => {
89
+ getDisplayName.mockReturnValue('John Doe ')
90
+ const contact = makeContact({
91
+ name: { familyName: 'Doe', givenName: 'John' }
92
+ })
93
+
94
+ render(<ContactName contact={contact} />)
95
+
96
+ const boldElement = document.querySelector('.u-fw-bold')
97
+ expect(boldElement).toHaveTextContent('Doe')
98
+ })
99
+ })