From 26ec7db329613ef5a5477c5f54e6736c1e1f2c71 Mon Sep 17 00:00:00 2001 From: Alda Vigdis Skarphedinsdottir Date: Tue, 26 Jun 2018 22:19:15 +0200 Subject: [PATCH] Adding date form to date picker and improving accessibility * Date form has been added to the date-time component, as a primary input method * Locale-dependent field order, with en-US being month-day, and the default day-month * Keyboard focus goes to the month selector first, as dates are validated based on the month (and year if leap-year) * The post-schedule sidebar widget is now more accessible, with button designated as a live object, so changes should be announced to date pickers * React-Datepicker widget has been removed from the keyboard navigation and screenreader context, as the new form fields are not the primary input method. * Tiny bit of code cleanup --- lib/client-assets.php | 7 +- package-lock.json | 267 ++++++++++++++---- packages/components/package.json | 2 +- packages/components/src/date-time/README.md | 15 +- packages/components/src/date-time/date.js | 60 +++- packages/components/src/date-time/index.js | 120 +++++++- packages/components/src/date-time/style.scss | 253 +++++++++++------ .../components/src/date-time/test/time.js | 105 ++++--- packages/components/src/date-time/time.js | 229 ++++++++++++--- packages/date/CHANGELOG.md | 6 + packages/date/src/index.js | 3 + .../components/sidebar/post-schedule/index.js | 41 ++- .../sidebar/post-schedule/style.scss | 4 + .../src/components/post-schedule/label.js | 3 +- test/e2e/specs/datepicker.test.js | 59 ++++ 15 files changed, 909 insertions(+), 265 deletions(-) create mode 100644 test/e2e/specs/datepicker.test.js diff --git a/lib/client-assets.php b/lib/client-assets.php index 0e1fb59f5b5d2..d48eaa69ac392 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -344,9 +344,10 @@ function gutenberg_register_scripts_and_styles() { ), ), 'formats' => array( - 'time' => get_option( 'time_format', __( 'g:i a', 'default' ) ), - 'date' => get_option( 'date_format', __( 'F j, Y', 'default' ) ), - 'datetime' => __( 'F j, Y g:i a', 'default' ), + 'time' => get_option( 'time_format', __( 'g:i a', 'default' ) ), + 'date' => get_option( 'date_format', __( 'F j, Y', 'default' ) ), + 'datetime' => __( 'F j, Y g:i a', 'default' ), + 'datetimeAbbreviated' => __( 'M j, Y g:i a', 'default' ), ), 'timezone' => array( 'offset' => get_option( 'gmt_offset', 0 ), diff --git a/package-lock.json b/package-lock.json index e8c8a25dcad5b..dcaccadef2965 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2143,7 +2143,7 @@ "re-resizable": "^4.7.1", "react-click-outside": "^2.3.1", "react-color": "^2.13.4", - "react-datepicker": "^1.4.1", + "react-dates": "^17.1.1", "rememo": "^3.0.0", "uuid": "^3.1.0" } @@ -2549,6 +2549,33 @@ "es6-promisify": "^5.0.0" } }, + "airbnb-prop-types": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.10.0.tgz", + "integrity": "sha512-M7kDqFO6kFNGV0fHPZaBx672m0jwbpCdbrtW2lcevCEuPB2sKCY3IPa030K/N1iJLEGwCNk4NSag65XBEulwhg==", + "requires": { + "array.prototype.find": "^2.0.4", + "function.prototype.name": "^1.1.0", + "has": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object.assign": "^4.1.0", + "object.entries": "^1.0.4", + "prop-types": "^15.6.1", + "prop-types-exact": "^1.1.2" + }, + "dependencies": { + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "requires": { + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + } + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -2780,6 +2807,25 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "array.prototype.find": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.0.4.tgz", + "integrity": "sha1-VWpcU2LAhkgyPdrrnenRS8GGTJA=", + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" + } + }, + "array.prototype.flat": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz", + "integrity": "sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==", + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.10.0", + "function-bind": "^1.1.1" + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -4225,6 +4271,11 @@ } } }, + "brcast": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brcast/-/brcast-2.0.2.tgz", + "integrity": "sha512-Tfn5JSE7hrUlFcOoaLzVvkbgIemIorMIyoMr3TgvszWW7jFt2C9PdeMLtysYD9RU0MmU17b69+XJG1eRY2OBRg==" + }, "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", @@ -5371,6 +5422,11 @@ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, + "consolidated-events": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/consolidated-events/-/consolidated-events-2.0.2.tgz", + "integrity": "sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==" + }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -6295,6 +6351,11 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==" + }, "default-require-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", @@ -6325,7 +6386,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "dev": true, "requires": { "foreach": "^2.0.5", "object-keys": "^1.0.8" @@ -6477,6 +6537,11 @@ "path-type": "^3.0.0" } }, + "direction": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.2.tgz", + "integrity": "sha512-hSKoz5FBn+zhP9vWKkVQaaxnRDg3/MoPdcg2au54HIUDR8MrP8Ah1jXSJwCXel6SV3Afh5DSzc8Uqv2r1UoQwQ==" + }, "discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", @@ -6818,7 +6883,6 @@ "version": "1.12.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", - "dev": true, "requires": { "es-to-primitive": "^1.1.1", "function-bind": "^1.1.1", @@ -6831,7 +6895,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "dev": true, "requires": { "is-callable": "^1.1.1", "is-date-object": "^1.0.1", @@ -7966,8 +8029,7 @@ "foreach": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" }, "forever-agent": { "version": "0.6.1", @@ -8590,14 +8652,12 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "function.prototype.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz", "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -9145,6 +9205,15 @@ "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", "dev": true }, + "global-cache": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/global-cache/-/global-cache-1.2.1.tgz", + "integrity": "sha512-EOeUaup5DgWKlCMhA9YFqNRIlZwoxt731jCh47WBV9fQqHgXhr3Fa55hfgIUqilIcPsfdNKN7LHjrNY+Km40KA==", + "requires": { + "define-properties": "^1.1.2", + "is-symbol": "^1.0.1" + } + }, "global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", @@ -9320,7 +9389,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -9363,8 +9431,7 @@ "has-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" }, "has-to-string-tag-x": { "version": "1.4.1", @@ -9844,8 +9911,7 @@ "is-callable": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" }, "is-ci": { "version": "1.1.0", @@ -9893,8 +9959,7 @@ "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, "is-decimal": { "version": "1.0.2", @@ -10121,7 +10186,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, "requires": { "has": "^1.0.1" } @@ -10188,8 +10252,7 @@ "is-symbol": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" }, "is-text-path": { "version": "1.0.1", @@ -10200,6 +10263,11 @@ "text-extensions": "^1.0.0" } }, + "is-touch-device": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-touch-device/-/is-touch-device-1.0.1.tgz", + "integrity": "sha512-LAYzo9kMT1b2p19L/1ATGt2XcSilnzNlyvq6c0pbPRVisLbAPpLqr53tIJS00kvrTkj0HtR8U7+u8X0yR8lPSw==" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -14423,14 +14491,12 @@ "object-is": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=" }, "object-keys": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", - "dev": true + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" }, "object-visit": { "version": "1.0.1", @@ -14445,7 +14511,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -14457,7 +14522,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", - "dev": true, "requires": { "define-properties": "^1.1.2", "es-abstract": "^1.6.1", @@ -14498,7 +14562,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", - "dev": true, "requires": { "define-properties": "^1.1.2", "es-abstract": "^1.6.1", @@ -15032,11 +15095,6 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, - "popper.js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.3.tgz", - "integrity": "sha1-FDj5jQRqz3tNeM1QK/QYrGTU8JU=" - }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -16342,6 +16400,16 @@ "loose-envify": "^1.3.1" } }, + "prop-types-exact": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", + "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", + "requires": { + "has": "^1.0.3", + "object.assign": "^4.1.0", + "reflect.ownkeys": "^0.2.0" + } + }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -16625,6 +16693,15 @@ } } }, + "react-addons-shallow-compare": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz", + "integrity": "sha1-GYoAuR/DdiPbZKKP0XtZa6NicC8=", + "requires": { + "fbjs": "^0.8.4", + "object-assign": "^4.1.0" + } + }, "react-autosize-textarea": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-3.0.2.tgz", @@ -16655,15 +16732,24 @@ "tinycolor2": "^1.1.2" } }, - "react-datepicker": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-1.4.1.tgz", - "integrity": "sha512-O/ExTWLS81pyWJWLFg1BRQEr9S/BDd6iMEkGctxQmVrRw2srW8DNdnQm5UgFNu8LoSZGMDvI55OghYZvDpWJhw==", + "react-dates": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/react-dates/-/react-dates-17.1.1.tgz", + "integrity": "sha512-kUQEf6AnXa0h067lW9teAYhzcSNjh8L1ZpUOdB1obzPTBTrzlSu94CvPOfTI43Rf+yiBWOXIpL36Ub+rKf4oKA==", "requires": { - "classnames": "^2.2.5", - "prop-types": "^15.6.0", - "react-onclickoutside": "^6.7.1", - "react-popper": "^0.9.1" + "airbnb-prop-types": "^2.10.0", + "consolidated-events": "^1.1.1 || ^2.0.0", + "is-touch-device": "^1.0.1", + "lodash": "^4.1.1", + "object.assign": "^4.1.0", + "object.values": "^1.0.4", + "prop-types": "^15.6.1", + "react-addons-shallow-compare": "^15.6.2", + "react-moment-proptypes": "^1.6.0", + "react-outside-click-handler": "^1.2.0", + "react-portal": "^4.1.5", + "react-with-styles": "^3.2.0", + "react-with-styles-interface-css": "^4.0.2" }, "dependencies": { "prop-types": { @@ -16705,17 +16791,22 @@ "integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==", "dev": true }, - "react-onclickoutside": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz", - "integrity": "sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg==" + "react-moment-proptypes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-moment-proptypes/-/react-moment-proptypes-1.6.0.tgz", + "integrity": "sha512-4h7EuhDMTzQqZ+02KUUO+AVA7PqhbD88yXB740nFpNDyDS/bj9jiPyn2rwr9sa8oDyaE1ByFN9+t5XPyPTmN6g==", + "requires": { + "moment": ">=1.6.0" + } }, - "react-popper": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.9.5.tgz", - "integrity": "sha1-AqJO8+7DOvnlToNYq3DrDjMe3QU=", + "react-outside-click-handler": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/react-outside-click-handler/-/react-outside-click-handler-1.2.2.tgz", + "integrity": "sha512-MgCxmFARGN1VrZdwoLkER/y3So6mC/fSniXI4XcXcB+Jt05nw/k8a/R1hSoa7p414uZUZ8NfClN3eVmZm9bM5Q==", "requires": { - "popper.js": "^1.14.1", + "airbnb-prop-types": "^2.10.0", + "consolidated-events": "^1.1.1 || ^2.0.0", + "object.values": "^1.0.4", "prop-types": "^15.6.1" }, "dependencies": { @@ -16730,6 +16821,14 @@ } } }, + "react-portal": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/react-portal/-/react-portal-4.1.5.tgz", + "integrity": "sha512-jJMy9DoVr4HRWPdO8IP/mDHP1Q972/aKkulVQeIrttOIyRNmCkR2IH7gK3HVjhzxy6M+k9TopSWN5q41wO/o6A==", + "requires": { + "prop-types": "^15.5.8" + } + }, "react-reconciler": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.7.0.tgz", @@ -16778,6 +16877,73 @@ } } }, + "react-with-direction": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.3.0.tgz", + "integrity": "sha512-2TflEebNckTNUybw3Rzqjg4BwM/H380ZL5lsbZ5f4UTY2JyE5uQdQZK5T2w+BDJSAMcqoA2RDJYa4e7Cl6C2Kg==", + "requires": { + "airbnb-prop-types": "^2.8.1", + "brcast": "^2.0.2", + "deepmerge": "^1.5.1", + "direction": "^1.0.1", + "hoist-non-react-statics": "^2.3.1", + "object.assign": "^4.1.0", + "object.values": "^1.0.4", + "prop-types": "^15.6.0" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "requires": { + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + } + } + }, + "react-with-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/react-with-styles/-/react-with-styles-3.2.1.tgz", + "integrity": "sha512-L+x/EDgrKkqV6pTfDtLMShf7Xs+bVQ+HAT5rByX88QYX+ft9t5Gn4PWMmg36Ur21IVEBMGjjQQIJGJpBrzbsyg==", + "requires": { + "deepmerge": "^1.5.2", + "hoist-non-react-statics": "^2.5.0", + "prop-types": "^15.6.1", + "react-with-direction": "^1.3.0" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "requires": { + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + } + } + }, + "react-with-styles-interface-css": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/react-with-styles-interface-css/-/react-with-styles-interface-css-4.0.3.tgz", + "integrity": "sha512-wE43PIyjal2dexxyyx4Lhbcb+E42amoYPnkunRZkb9WTA+Z+9LagbyxwsI352NqMdFmghR0opg29dzDO4/YXbw==", + "requires": { + "array.prototype.flat": "^1.2.1", + "global-cache": "^1.2.1" + } + }, "reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -17042,6 +17208,11 @@ "resolved": "https://registry.npmjs.org/redux-optimist/-/redux-optimist-1.0.0.tgz", "integrity": "sha512-AG1v8o6UZcGXTEH2jVcWG6KD+gEix+Cj9JXAAzln9MPkauSVd98H7N7EOOyT/v4c9N1mJB4sm1zfspGlLDkUEw==" }, + "reflect.ownkeys": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", + "integrity": "sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=" + }, "refx": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/refx/-/refx-3.0.0.tgz", diff --git a/packages/components/package.json b/packages/components/package.json index 60f71a2e1416c..704649de34864 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -43,7 +43,7 @@ "re-resizable": "^4.7.1", "react-click-outside": "^2.3.1", "react-color": "^2.13.4", - "react-datepicker": "^1.4.1", + "react-dates": "^17.1.1", "rememo": "^3.0.0", "uuid": "^3.1.0" }, diff --git a/packages/components/src/date-time/README.md b/packages/components/src/date-time/README.md index 9193617331d90..d5a2f26246e43 100644 --- a/packages/components/src/date-time/README.md +++ b/packages/components/src/date-time/README.md @@ -11,13 +11,12 @@ import { DateTimePicker } from '@wordpress/components'; import { getSettings } from '@wordpress/date'; import { withState } from '@wordpress/compose'; - const MyDateTimePicker = withState( { date: new Date(), } )( ( { date, setState } ) => { const settings = getSettings(); - // To know if the current timezone is a 12 hour time with look for "a" in the time format. + // To know if the current timezone is a 12 hour time with look for an "a" in the time format. // We also make sure this a is not escaped by a "/". const is12HourTime = /a(?!\\)/i.test( settings.formats.time @@ -28,11 +27,11 @@ const MyDateTimePicker = withState( { return ( setState( { date } ) } - locale={ settings.l10n.locale } - is12Hour={ is12HourTime } - /> + currentDate={ date } + onChange={ ( date ) => setState( { date } ) } + locale={ settings.l10n.locale } + is12Hour={ is12HourTime } + /> ); } ); ``` @@ -65,7 +64,7 @@ The localization for the display of the date and time. ### is12Hour -Whether the current timezone is a 12 hour time. +Whether we use a 12-hour clock. With a 12-hour clock, an AM/PM widget is displayed and the time format is assumed to be MM-DD-YYYY. - Type: `bool` - Required: No diff --git a/packages/components/src/date-time/date.js b/packages/components/src/date-time/date.js index 41819664e4474..c23d5a0f105b4 100644 --- a/packages/components/src/date-time/date.js +++ b/packages/components/src/date-time/date.js @@ -1,24 +1,64 @@ /** * External dependencies */ -import ReactDatePicker from 'react-datepicker'; import moment from 'moment'; +import { DayPickerSingleDateController } from 'react-dates'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; /** * Module Constants */ const TIMEZONELESS_FORMAT = 'YYYY-MM-DDTHH:mm:ss'; -function DatePicker( { currentDate, onChange, ...args } ) { - const momentDate = currentDate ? moment( currentDate ) : moment(); - const onChangeMoment = ( newDate ) => onChange( newDate.format( TIMEZONELESS_FORMAT ) ); +class DatePicker extends Component { + constructor() { + super( ...arguments ); + + this.onChangeMoment = this.onChangeMoment.bind( this ); + } + + onChangeMoment( newDate ) { + const { currentDate, onChange } = this.props; + + const momentDate = currentDate ? moment( currentDate ) : moment(); + const momentTime = { + hours: momentDate.hours(), + minutes: momentDate.minutes(), + seconds: momentDate.seconds(), + }; + + onChange( newDate.set( momentTime ).format( TIMEZONELESS_FORMAT ) ); + } + + render() { + const { currentDate } = this.props; + + const momentDate = currentDate ? moment( currentDate ) : moment(); - return ; + return ( +
+ +
+ ); + } } export default DatePicker; diff --git a/packages/components/src/date-time/index.js b/packages/components/src/date-time/index.js index 80ee3174d6d22..9ee8eaa527f54 100644 --- a/packages/components/src/date-time/index.js +++ b/packages/components/src/date-time/index.js @@ -1,24 +1,114 @@ +/** + * External dependencies + */ +// Needed to initialise the default datepicker styles. +// See: https://github.com/airbnb/react-dates#initialize +import 'react-dates/initialize'; + +/** + * WordPress dependencies + */ +import { Component, Fragment } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ +import Button from '../button'; import { default as DatePicker } from './date'; import { default as TimePicker } from './time'; export { DatePicker, TimePicker }; -export function DateTimePicker( { currentDate, onChange, is12Hour, ...args } ) { - return [ - , - , - ]; +export class DateTimePicker extends Component { + constructor() { + super( ...arguments ); + + this.state = { calendarHelpIsVisible: false }; + + this.onClickDescriptionToggle = this.onClickDescriptionToggle.bind( this ); + } + + onClickDescriptionToggle() { + this.setState( { calendarHelpIsVisible: ! this.state.calendarHelpIsVisible } ); + } + + render() { + const { currentDate, is12Hour, locale, onChange } = this.props; + + return ( +
+ { ! this.state.calendarHelpIsVisible && ( + + + + + ) } + + { this.state.calendarHelpIsVisible && ( + +
+

{ __( 'Click to Select' ) }

+
    +
  • { __( 'Click the right and left arrows to select other months in the past or the future.' ) }
  • +
  • { __( 'Click the desired day to select it.' ) }
  • +
+ +

{ __( 'Navigating with a keyboard' ) }

+
    +
  • + + { ' ' /* JSX removes whitespace, but a space is required for screen readers. */ } + { __( 'Select the date in focus.' ) } +
  • +
  • + ←/→ + { ' ' /* JSX removes whitespace, but a space is required for screen readers. */ } + { __( 'Move backward (left) and forward (right) by one day.' ) } +
  • +
  • + ↑/↓ + { ' ' /* JSX removes whitespace, but a space is required for screen readers. */ } + { __( 'Move backward (up) and forward (down) by one week.
  • ' ) } + +
  • + { __( 'PgUp/PgDn' ) } + { ' ' /* JSX removes whitespace, but a space is required for screen readers. */ } + { __( 'Move backward (PgUp) or forward (PgDn) by one month.' ) } +
  • +
  • + { __( 'Home/End' ) } + { ' ' /* JSX removes whitespace, but a space is required for screen readers. */ } + { __( 'Go to the first (home) or last (end) day of a week.' ) } +
  • +
+ + +
+
+ ) } + + { ! this.state.calendarHelpIsVisible && ( + + ) } +
+ ); + } } diff --git a/packages/components/src/date-time/style.scss b/packages/components/src/date-time/style.scss index a0738635acc83..6718ea8a63b43 100644 --- a/packages/components/src/date-time/style.scss +++ b/packages/components/src/date-time/style.scss @@ -1,126 +1,195 @@ -$datepicker__border-radius: 0; -$datepicker__day-margin: 0.166rem; -$datepicker__font-size: 13px; -$datepicker__font-family: inherit; -$datepicker__item-size: 28px; -$datepicker__margin: 0.4rem; -$datepicker__navigation-size: 6px; -$datepicker__triangle-size: 6px; - -$datepicker__background-color: $light-gray-300; -$datepicker__text-color: $dark-gray-500; -$datepicker__header-color: $black; -$datepicker__muted-color: #ccc; -$datepicker__navigation-disabled-color: lighten($datepicker__muted-color, 10%); -$datepicker__border-color: $light-gray-500; - -// Lerna hoist packages, so can't reference with ~ -@import "../../../../node_modules/react-datepicker/src/stylesheets/datepicker"; - -.react-datepicker__month-container { - float: none; -} - -.react-datepicker-time__header, -.react-datepicker__current-month { - font-size: $datepicker__font-size; -} +// We can't reference this package with ~ because of how Lerna handles packages. 😩 +@import "../../node_modules/react-dates/lib/css/_datepicker.css"; -.react-datepicker__navigation { - top: 12px; +.components-datetime { + .components-datetime__calendar-help { + padding: $grid-size; - &--previous, - &--previous:hover { - border-right-color: $black; + h4 { + margin: 0; + } } - &--next, - &--next:hover { - border-left-color: $black; + + .components-datetime__date-help-button { + display: block; + margin-left: auto; + margin-right: $grid-size; + margin-top: 0.5em; } } -.components-time-picker { - display: flex; - align-items: center; - justify-content: center; - margin-top: 10px; - max-width: 248px; +.components-datetime__date { + min-height: 236px; + border-top: 1px solid $light-gray-500; + margin-left: -$grid-size; + margin-right: -$grid-size; - .components-time-picker__input { - width: 40px; + // Override external DatePicker styles. + .CalendarMonth_caption { + font-size: $default-font-size; } - .components-time-picker__separator { - padding: 0 4px; + .CalendarDay { + font-size: $default-font-size; + border: 1px solid transparent; + border-radius: $radius-round; + text-align: center; } - .components-time-picker__am-button { - margin-left: 8px; - margin-right: -1px; - border-radius: 3px 0 0 3px; + .CalendarDay__selected { + background: theme(primary); + + &:hover { + background: theme(primary) shade(15%); + } + } + + .DayPickerNavigation_button__horizontalDefault { + padding: 2px 8px; + top: 20px; } - .components-time-picker__pm-button { - margin-left: -1px; - border-radius: 0 3px 3px 0; + .DayPicker_weekHeader { + top: 50px; } - .components-time-picker__am-button.is-toggled, - .components-time-picker__pm-button.is-toggled { - background: $light-gray-300; - border-color: $dark-gray-100; - box-shadow: inset 0 2px 5px -3px $dark-gray-500; - transform: translateY(1px); + &.is-description-visible .DayPicker, + &.is-description-visible .components-datetime__date-help-button { + visibility: hidden; } } -// Extending colors to use theme colors -.react-datepicker__time-container { - .react-datepicker__time { - .react-datepicker__time-box { - ul.react-datepicker__time-list { - li.react-datepicker__time-list-item { - &--selected { - background-color: theme(primary); - &:hover { - background-color: theme(primary); - } - } +.components-datetime__time { + margin-bottom: 1em; + + fieldset { + margin-top: 0.5em; + position: relative; + } + + .components-datetime__time-field-am-pm fieldset { + margin-top: 0; + } + + .components-datetime__time-wrapper { + display: flex; + + .components-datetime__time-separator { + display: inline-block; + padding: 0 3px 0 0; + color: $dark-gray-500; + } + + .components-datetime__time-am-button { + margin-left: 8px; + margin-right: -1px; + border-radius: 3px 0 0 3px; + } + + .components-datetime__time-pm-button { + margin-left: -1px; + border-radius: 0 3px 3px 0; + } + + .components-datetime__time-am-button.is-toggled, + .components-datetime__time-pm-button.is-toggled { + background: $light-gray-300; + border-color: $dark-gray-100; + box-shadow: inset 0 2px 5px -3px $dark-gray-500; + } + + .components-datetime__time-field { + align-self: center; + flex: 0 1 auto; + order: 1; + + &.am-pm button { + font-size: 11px; + font-weight: bold; + } + + select { + padding: 2px; + margin-right: 4px; + + &:focus { + position: relative; + z-index: 1; } } - } - } -} -.react-datepicker__day { - &--highlighted { - background-color: theme(primary); - &:hover { - background-color: color(theme(primary) shade(5%)); + input[type="number"] { + padding: 2px; + margin-right: 4px; + width: 40px; + text-align: center; + -moz-appearance: textfield; + + &:focus { + position: relative; + z-index: 1; + } + + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + } } } - &--selected, - &--in-selecting-range, - &--in-range { - background-color: theme(primary); - &:hover { - background-color: color(theme(primary) shade(5%)); + // Makes the month appear before the day if time format uses AM/PM + // We are assuming MM-DD-YYY corelates with AM/PM + &.is-12-hour { + .components-datetime__time-field-day input { + margin: 0 -4px 0 0 !important; + border-radius: $radius-round-rectangle 0 0 $radius-round-rectangle !important; + } + .components-datetime__time-field-year input { + border-radius: 0 $radius-round-rectangle $radius-round-rectangle 0 !important; } } - &--keyboard-selected { - background-color: color(theme(primary) tint(5%)); - &:hover { - background-color: color(theme(primary) shade(5%)); + &.is-24-hour { + .components-datetime__time-field-day { + order: 0 !important; } } +} - &--in-selecting-range:not(&--in-range) { - background-color: color(theme(primary) a(50%)); +.components-datetime__time-legend { + font-weight: 600; + margin-top: 0.5em; + + &.invisible { + position: absolute; + top: -999em; + left: -999em; } } -.react-datepicker__close-icon::after { - background-color: theme(primary); +.components-datetime__time-field-hours-input, +.components-datetime__time-field-minutes-input, +.components-datetime__time-field-day-input { + width: 35px; +} + +.components-datetime__time-field-year-input { + width: 55px; +} + +.components-datetime__time-field-month-select { + width: 90px; +} + +// Hack to center the datepicker component within the popover. +// It sets its own styles so centering is tricky. +.components-popover .components-datetime__date { + padding-left: 6px; +} + +// Used to prevent z-index issues on mobile. +// See: https://github.com/WordPress/gutenberg/pull/7621#issuecomment-424322735 +.components-popover.edit-post-post-schedule__dialog.is-bottom.is-left { + z-index: 100000; } diff --git a/packages/components/src/date-time/test/time.js b/packages/components/src/date-time/test/time.js index 8e50c1c994f5c..64c868ea293aa 100644 --- a/packages/components/src/date-time/test/time.js +++ b/packages/components/src/date-time/test/time.js @@ -9,64 +9,105 @@ import { shallow } from 'enzyme'; import TimePicker from '../time'; describe( 'TimePicker', () => { - it( 'should not call onChange if we enter dummy text', () => { + it( 'should have the correct CSS class if 12-hour clock is specified', () => { const onChangeSpy = jest.fn(); - const button = shallow( ); - const input = button.find( '.components-time-picker__input' ).at( 0 ); - input.simulate( 'change', { target: { value: 'My new value' } } ); - input.simulate( 'blur' ); - expect( onChangeSpy ).not.toHaveBeenCalled(); + const picker = shallow( ); + expect( picker.hasClass( 'is-12-hour' ) ).toBe( true ); } ); - it( 'should call onChange with the updated hours', () => { + it( 'should have the correct CSS class if 24-hour clock is specified', () => { const onChangeSpy = jest.fn(); - const button = shallow( ); - const input = button.find( '.components-time-picker__input' ).at( 0 ); + const picker = shallow( + + ); + expect( picker.hasClass( 'is-24-hour' ) ).toBe( true ); + } ); + + it( 'should call onChange with an updated day', () => { + const onChangeSpy = jest.fn(); + const picker = shallow( ); + const input = picker.find( '.components-datetime__time-field-day-input' ).at( 0 ); input.simulate( 'change', { target: { value: '10' } } ); input.simulate( 'blur' ); - expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T10:00:00' ); + expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-10T11:00:00' ); } ); - it( 'should call onChange with the updated minutes', () => { + it( 'should call onChange with an updated month', () => { const onChangeSpy = jest.fn(); - const button = shallow( ); - const input = button.find( '.components-time-picker__input' ).at( 1 ); - input.simulate( 'change', { target: { value: '10' } } ); + const picker = shallow( ); + const input = picker.find( '.components-datetime__time-field-month-select' ).at( 0 ); + input.simulate( 'change', { target: { value: '12' } } ); input.simulate( 'blur' ); - expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T11:10:00' ); + expect( onChangeSpy ).toHaveBeenCalledWith( '1986-12-18T11:00:00' ); } ); - it( 'should switch to PM properly', () => { + it( 'should call onChange with an updated year', () => { const onChangeSpy = jest.fn(); - const button = shallow( ); - const pmButton = button.find( '.components-time-picker__pm-button' ); - pmButton.simulate( 'click' ); - expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T23:00:00' ); + const picker = shallow( ); + const input = picker.find( '.components-datetime__time-field-year-input' ).at( 0 ); + input.simulate( 'change', { target: { value: '2018' } } ); + input.simulate( 'blur' ); + expect( onChangeSpy ).toHaveBeenCalledWith( '2018-10-18T11:00:00' ); } ); - it( 'should switch to AM properly', () => { + it( 'should call onChange with an updated hour (12-hour clock)', () => { const onChangeSpy = jest.fn(); - const button = shallow( ); - const pmButton = button.find( '.components-time-picker__am-button' ); - pmButton.simulate( 'click' ); - expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T11:00:00' ); + const picker = shallow( ); + const input = picker.find( '.components-datetime__time-field-hours-input' ).at( 0 ); + input.simulate( 'change', { target: { value: '10' } } ); + input.simulate( 'blur' ); + expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T10:00:00' ); } ); - it( 'should call onChange with the updated hours (24 hours time)', () => { + it( 'should not call onChange with an updated hour (12-hour clock) if the hour is out of bounds', () => { const onChangeSpy = jest.fn(); - const button = shallow( ); - const input = button.find( '.components-time-picker__input' ).at( 0 ); + const picker = shallow( ); + const input = picker.find( '.components-datetime__time-field-hours-input' ).at( 0 ); input.simulate( 'change', { target: { value: '22' } } ); input.simulate( 'blur' ); - expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T22:00:00' ); + expect( onChangeSpy ).not.toHaveBeenCalled(); } ); - it( 'should not call onChange with the updated hours if invalid 12 hour', () => { + it( 'should call onChange with an updated hour (24-hour clock)', () => { const onChangeSpy = jest.fn(); - const button = shallow( ); - const input = button.find( '.components-time-picker__input' ).at( 0 ); + const picker = shallow( ); + const input = picker.find( '.components-datetime__time-field-hours-input' ).at( 0 ); input.simulate( 'change', { target: { value: '22' } } ); input.simulate( 'blur' ); + expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T22:00:00' ); + } ); + + it( 'should call onChange with an updated minute', () => { + const onChangeSpy = jest.fn(); + const picker = shallow( ); + const input = picker.find( '.components-datetime__time-field-minutes-input' ).at( 0 ); + input.simulate( 'change', { target: { value: '10' } } ); + input.simulate( 'blur' ); + expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T11:10:00' ); + } ); + + it( 'should not call onChange with an updated minute if out of bounds', () => { + const onChangeSpy = jest.fn(); + const picker = shallow( ); + const input = picker.find( '.components-datetime__time-field-minutes-input' ).at( 0 ); + input.simulate( 'change', { target: { value: '99' } } ); + input.simulate( 'blur' ); expect( onChangeSpy ).not.toHaveBeenCalled(); } ); + + it( 'should switch to PM correctly', () => { + const onChangeSpy = jest.fn(); + const button = shallow( ); + const pmButton = button.find( '.components-datetime__time-pm-button' ); + pmButton.simulate( 'click' ); + expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T23:00:00' ); + } ); + + it( 'should switch to AM correctly', () => { + const onChangeSpy = jest.fn(); + const button = shallow( ); + const pmButton = button.find( '.components-datetime__time-am-button' ); + pmButton.simulate( 'click' ); + expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T11:00:00' ); + } ); } ); diff --git a/packages/components/src/date-time/time.js b/packages/components/src/date-time/time.js index 32bb7228d14ab..1db16f6b362d2 100644 --- a/packages/components/src/date-time/time.js +++ b/packages/components/src/date-time/time.js @@ -1,14 +1,15 @@ /** * External dependencies */ +import classnames from 'classnames'; import { isInteger } from 'lodash'; import moment from 'moment'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -24,11 +25,20 @@ class TimePicker extends Component { constructor() { super( ...arguments ); this.state = { + day: '', + month: '', + year: '', hours: '', minutes: '', am: true, date: null, }; + this.updateMonth = this.updateMonth.bind( this ); + this.onChangeMonth = this.onChangeMonth.bind( this ); + this.updateDay = this.updateDay.bind( this ); + this.onChangeDay = this.onChangeDay.bind( this ); + this.updateYear = this.updateYear.bind( this ); + this.onChangeYear = this.onChangeYear.bind( this ); this.updateHours = this.updateHours.bind( this ); this.updateMinutes = this.updateMinutes.bind( this ); this.onChangeHours = this.onChangeHours.bind( this ); @@ -49,13 +59,24 @@ class TimePicker extends Component { } } + getMaxHours() { + return this.props.is12Hour ? 12 : 23; + } + + getMinHours() { + return this.props.is12Hour ? 1 : 0; + } + syncState( { currentTime, is12Hour } ) { const selected = currentTime ? moment( currentTime ) : moment(); + const day = selected.format( 'DD' ); + const month = selected.format( 'MM' ); + const year = selected.format( 'YYYY' ); const minutes = selected.format( 'mm' ); const am = selected.format( 'A' ); const hours = selected.format( is12Hour ? 'hh' : 'HH' ); const date = currentTime ? moment( currentTime ) : moment(); - this.setState( { minutes, hours, am, date } ); + this.setState( { day, month, year, minutes, hours, am, date } ); } updateHours() { @@ -75,8 +96,7 @@ class TimePicker extends Component { date.clone().hours( am === 'AM' ? value % 12 : ( ( ( value % 12 ) + 12 ) % 24 ) ) : date.clone().hours( value ); this.setState( { date: newDate } ); - const formattedDate = newDate.format( TIMEZONELESS_FORMAT ); - onChange( formattedDate ); + onChange( newDate.format( TIMEZONELESS_FORMAT ) ); } updateMinutes() { @@ -89,8 +109,46 @@ class TimePicker extends Component { } const newDate = date.clone().minutes( value ); this.setState( { date: newDate } ); - const formattedDate = newDate.format( TIMEZONELESS_FORMAT ); - onChange( formattedDate ); + onChange( newDate.format( TIMEZONELESS_FORMAT ) ); + } + + updateDay() { + const { onChange } = this.props; + const { day, date } = this.state; + const value = parseInt( day, 10 ); + if ( ! isInteger( value ) || value < 1 || value > 31 ) { + this.syncState( this.props ); + return; + } + const newDate = date.clone().date( value ); + this.setState( { date: newDate } ); + onChange( newDate.format( TIMEZONELESS_FORMAT ) ); + } + + updateMonth() { + const { onChange } = this.props; + const { month, date } = this.state; + const value = parseInt( month, 10 ); + if ( ! isInteger( value ) || value < 1 || value > 12 ) { + this.syncState( this.props ); + return; + } + const newDate = date.clone().month( value - 1 ); + this.setState( { date: newDate } ); + onChange( newDate.format( TIMEZONELESS_FORMAT ) ); + } + + updateYear() { + const { onChange } = this.props; + const { year, date } = this.state; + const value = parseInt( year, 10 ); + if ( ! isInteger( value ) || value < 1970 || value > 9999 ) { + this.syncState( this.props ); + return; + } + const newDate = date.clone().year( value ); + this.setState( { date: newDate } ); + onChange( newDate.format( TIMEZONELESS_FORMAT ) ); } updateAmPm( value ) { @@ -107,11 +165,22 @@ class TimePicker extends Component { newDate = date.clone().hours( parseInt( hours, 10 ) % 12 ); } this.setState( { date: newDate } ); - const formattedDate = newDate.format( TIMEZONELESS_FORMAT ); - onChange( formattedDate ); + onChange( newDate.format( TIMEZONELESS_FORMAT ) ); }; } + onChangeDay( event ) { + this.setState( { day: event.target.value } ); + } + + onChangeMonth( event ) { + this.setState( { month: event.target.value } ); + } + + onChangeYear( event ) { + this.setState( { year: event.target.value } ); + } + onChangeHours( event ) { this.setState( { hours: event.target.value } ); } @@ -122,43 +191,117 @@ class TimePicker extends Component { render() { const { is12Hour } = this.props; - const { minutes, hours, am } = this.state; + const { day, month, year, minutes, hours, am } = this.state; return ( -
- - : - - { is12Hour &&
- - -
} +
+
+ { __( 'Date' ) } +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ { __( 'Time' ) } +
+
+ + + +
+ { is12Hour && ( +
+ + +
+ ) } +
+
); } diff --git a/packages/date/CHANGELOG.md b/packages/date/CHANGELOG.md index a49cefcd0d75c..28d8eb0727af4 100644 --- a/packages/date/CHANGELOG.md +++ b/packages/date/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.1.0 (2018-09-26) + +### New Features + +- Added a `datetimeAbbreviated` format to `getSettings().format` for abbreviated months. + ## 2.0.0 (2018-09-05) ### Breaking Change diff --git a/packages/date/src/index.js b/packages/date/src/index.js index d3dcdabcd6414..96512152dbbf6 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.js @@ -5,6 +5,8 @@ import momentLib from 'moment'; import 'moment-timezone'; import 'moment-timezone/moment-timezone-utils'; +// Changes made here will likely need to be made in `lib/client-assets.php` as +// well because it uses the `setSettings()` function to change these settings. let settings = { l10n: { locale: 'en_US', @@ -19,6 +21,7 @@ let settings = { time: 'g: i a', date: 'F j, Y', datetime: 'F j, Y g: i a', + datetimeAbbreviated: 'M j, Y g: i a', }, timezone: { offset: '0', string: '' }, }; diff --git a/packages/edit-post/src/components/sidebar/post-schedule/index.js b/packages/edit-post/src/components/sidebar/post-schedule/index.js index e604369245e2a..70ae683fa86b5 100644 --- a/packages/edit-post/src/components/sidebar/post-schedule/index.js +++ b/packages/edit-post/src/components/sidebar/post-schedule/index.js @@ -3,26 +3,43 @@ */ import { __ } from '@wordpress/i18n'; import { PanelRow, Dropdown, Button } from '@wordpress/components'; +import { withInstanceId } from '@wordpress/compose'; import { PostSchedule as PostScheduleForm, PostScheduleLabel, PostScheduleCheck } from '@wordpress/editor'; +import { Fragment } from '@wordpress/element'; -export function PostSchedule() { +export function PostSchedule( { instanceId } ) { return ( - { __( 'Publish' ) } + ( - + + + + ) } renderContent={ () => } /> @@ -31,4 +48,4 @@ export function PostSchedule() { ); } -export default PostSchedule; +export default withInstanceId( PostSchedule ); diff --git a/packages/edit-post/src/components/sidebar/post-schedule/style.scss b/packages/edit-post/src/components/sidebar/post-schedule/style.scss index 800fe7c6e6d24..417591bb68c32 100644 --- a/packages/edit-post/src/components/sidebar/post-schedule/style.scss +++ b/packages/edit-post/src/components/sidebar/post-schedule/style.scss @@ -3,6 +3,10 @@ position: relative; } +.edit-post-post-schedule__label { + display: none; +} + .components-button.edit-post-post-schedule__toggle { text-align: right; } diff --git a/packages/editor/src/components/post-schedule/label.js b/packages/editor/src/components/post-schedule/label.js index dce2969b49c61..584324e55249a 100644 --- a/packages/editor/src/components/post-schedule/label.js +++ b/packages/editor/src/components/post-schedule/label.js @@ -7,8 +7,9 @@ import { withSelect } from '@wordpress/data'; export function PostScheduleLabel( { date, isFloating } ) { const settings = getSettings(); + return date && ! isFloating ? - dateI18n( settings.formats.datetime, date ) : + dateI18n( settings.formats.datetimeAbbreviated, date ) : __( 'Immediately' ); } diff --git a/test/e2e/specs/datepicker.test.js b/test/e2e/specs/datepicker.test.js new file mode 100644 index 0000000000000..11bd817098098 --- /dev/null +++ b/test/e2e/specs/datepicker.test.js @@ -0,0 +1,59 @@ +/** + * Internal dependencies + */ +import { newPost } from '../support/utils'; + +describe( 'Datepicker', () => { + beforeEach( async () => { + await newPost(); + } ); + + it( 'should show the publishing date as "Immediately" if the date is not altered', async () => { + const publishingDate = await page.$eval( + '.edit-post-post-schedule__toggle', + ( dateLabel ) => dateLabel.textContent + ); + + expect( publishingDate ).toEqual( 'Immediately' ); + } ); + + it( 'should show the publishing date if the date is in the past', async () => { + // Open the datepicker. + await page.click( '.edit-post-post-schedule__toggle' ); + + // Change the publishing date to a year in the past. + await page.click( '.components-datetime__time-field-year' ); + await page.keyboard.press( 'ArrowDown' ); + + // Close the datepicker. + await page.click( '.edit-post-post-schedule__toggle' ); + + const publishingDate = await page.$eval( + '.edit-post-post-schedule__toggle', + ( dateLabel ) => dateLabel.textContent + ); + + expect( publishingDate ).toMatch( /[A-Za-z]{3} \d{1,2}, \d{4} \d{1,2}:\d{2} [ap]m/ ); + } ); + + it( 'should show the publishing date if the date is in the future', async () => { + // Open the datepicker. + await page.click( '.edit-post-post-schedule__toggle' ); + + // Change the publishing date to a year in the future. + await page.click( '.components-datetime__time-field-year' ); + await page.keyboard.press( 'ArrowUp' ); + + // Close the datepicker. + await page.click( '.edit-post-post-schedule__toggle' ); + + const publishingDate = await page.$eval( + '.edit-post-post-schedule__toggle', + ( dateLabel ) => dateLabel.textContent + ); + + expect( publishingDate ).not.toEqual( 'Immediately' ); + // The expected date format will be "Sep 26, 2018 11:52 pm". + expect( publishingDate ).toMatch( /[A-Za-z]{3} \d{1,2}, \d{4} \d{1,2}:\d{2} [ap]m/ ); + } ); +} );