diff --git a/assets/src/js/_acf-field-color-picker.js b/assets/src/js/_acf-field-color-picker.js index 3974c26e..2556accc 100644 --- a/assets/src/js/_acf-field-color-picker.js +++ b/assets/src/js/_acf-field-color-picker.js @@ -20,15 +20,32 @@ return this.$( 'input[type="text"]' ); }, + $inputNative: function () { + return this.$( 'input[type="color"]' ); + }, + setValue: function ( val ) { // update input (with change) acf.val( this.$input(), val ); + // Native input only needs the hex value. + var $native = this.$inputNative(); + if ( $native.length ) { + $native.val( val || '' ); + return; + } + // update iris this.$inputText().iris( 'color', val ); }, initialize: function () { + // Native input: the browser provides the picker, we only keep the + // hidden input in sync with the hex value the input reports. + if ( this.$control().data( 'native' ) === 1 ) { + return this.initializeNative(); + } + // vars var $input = this.$input(); var $inputText = this.$inputText(); @@ -73,7 +90,21 @@ $inputText.wpColorPicker( args ); }, + initializeNative: function () { + var self = this; + var $native = this.$inputNative(); + + $native.on( 'change input', function () { + acf.val( self.$input(), $native.val() ); + } ); + }, + onDuplicate: function ( e, $el, $duplicate ) { + // Native inputs carry no generated markup to clean up. + if ( this.$inputNative().length ) { + return; + } + // The wpColorPicker library does not provide a destroy method. // Manually reset DOM by replacing elements back to their original state. $colorPicker = $duplicate.find( '.wp-picker-container' ); diff --git a/assets/src/js/_acf-field-date-picker.js b/assets/src/js/_acf-field-date-picker.js index f1747aab..9b0a8540 100644 --- a/assets/src/js/_acf-field-date-picker.js +++ b/assets/src/js/_acf-field-date-picker.js @@ -19,7 +19,17 @@ return this.$( 'input[type="text"]' ); }, + $inputNative: function () { + return this.$( 'input[type="date"]' ); + }, + initialize: function () { + // Native input: the browser provides the picker, we only keep the + // hidden input in sync with the ISO value the input reports. + if ( this.$control().data( 'native' ) === 1 ) { + return this.initializeNative(); + } + // save_format: compatibility with ACF < 5.0.0 if ( this.has( 'save_format' ) ) { return this.initializeCompatibility(); @@ -76,6 +86,22 @@ acf.doAction( 'date_picker_init', $inputText, args, this ); }, + initializeNative: function () { + var self = this; + var $native = this.$inputNative(); + + $native.on( 'change input', function () { + self.syncNative(); + } ); + }, + + syncNative: function () { + var val = this.$inputNative().val(); + + // The stored format is Ymd, the native input reports Y-m-d. + acf.val( this.$input(), val ? val.replace( /-/g, '' ) : '' ); + }, + initializeCompatibility: function () { // vars var $input = this.$input(); @@ -118,6 +144,13 @@ setValue: function ( val ) { acf.val( this.$input(), val ); + // Native input expects the ISO value. + var $native = this.$inputNative(); + if ( $native.length ) { + $native.val( val ? this.toNative( val ) : '' ); + return; + } + const $inputText = this.$inputText(); if ( val && $inputText.length ) { try { @@ -136,6 +169,20 @@ } }, + toNative: function ( val ) { + // Ymd to Y-m-d + if ( /^\d{8}$/.test( val ) ) { + return ( + val.substring( 0, 4 ) + + '-' + + val.substring( 4, 6 ) + + '-' + + val.substring( 6, 8 ) + ); + } + return val; + }, + onBlur: function () { if ( ! this.$inputText().val() ) { acf.val( this.$input(), '' ); @@ -143,6 +190,11 @@ }, onDuplicate: function ( e, $el, $duplicate ) { + // Native inputs carry no generated markup to clean up. + if ( this.$control().data( 'native' ) === 1 ) { + return; + } + $duplicate .find( 'input[type="text"]' ) .removeClass( 'hasDatepicker' ) diff --git a/assets/src/js/_acf-field-date-time-picker.js b/assets/src/js/_acf-field-date-time-picker.js index 64478a4a..453a0887 100644 --- a/assets/src/js/_acf-field-date-time-picker.js +++ b/assets/src/js/_acf-field-date-time-picker.js @@ -6,7 +6,40 @@ return this.$( '.acf-date-time-picker' ); }, + $inputNative: function () { + return this.$( 'input[type="datetime-local"]' ); + }, + + syncNative: function () { + var val = this.$inputNative().val(); + + if ( val ) { + // The stored format is Y-m-d H:i:s, the input reports Y-m-dTH:i(:s). + val = val.replace( 'T', ' ' ); + + // The input can report HH:MM, but the stored value always has seconds. + if ( /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test( val ) ) { + val += ':00'; + } + } + + acf.val( this.$input(), val ); + }, + + toNative: function ( val ) { + // Y-m-d H:i:s to Y-m-dTH:i:s + return val.indexOf( ' ' ) !== -1 ? val.replace( ' ', 'T' ) : val; + }, + setValue: function ( val ) { + // The native input only needs the ISO value, the parent handles the rest. + var $native = this.$inputNative(); + if ( $native.length ) { + acf.val( this.$input(), val ); + $native.val( val ? this.toNative( val ) : '' ); + return; + } + acf.val( this.$input(), val ); const $inputText = this.$inputText(); @@ -52,6 +85,12 @@ }, initialize: function () { + // Native input: the browser provides the picker, we only keep the + // hidden input in sync with the ISO value the input reports. + if ( this.$control().data( 'native' ) === 1 ) { + return this.initializeNative(); + } + // vars var $input = this.$input(); var $inputText = this.$inputText(); @@ -119,6 +158,14 @@ // action acf.doAction( 'date_time_picker_init', $inputText, args, this ); }, + + initializeNative: function () { + var self = this; + + this.$inputNative().on( 'change input', function () { + self.syncNative(); + } ); + }, } ); acf.registerFieldType( Field ); diff --git a/assets/src/js/_acf-field-time-picker.js b/assets/src/js/_acf-field-time-picker.js index f8fc883d..b3cdadac 100644 --- a/assets/src/js/_acf-field-time-picker.js +++ b/assets/src/js/_acf-field-time-picker.js @@ -6,9 +6,31 @@ return this.$( '.acf-time-picker' ); }, + $inputNative: function () { + return this.$( 'input[type="time"]' ); + }, + + syncNative: function () { + var val = this.$inputNative().val(); + + // The input can report HH:MM, but the stored value is always H:i:s. + if ( /^\d{2}:\d{2}$/.test( val ) ) { + val += ':00'; + } + + acf.val( this.$input(), val ); + }, + setValue: function ( val ) { acf.val( this.$input(), val ); + // Native input only needs the stored value, it is already ISO. + var $native = this.$inputNative(); + if ( $native.length ) { + $native.val( val || '' ); + return; + } + const $inputText = this.$inputText(); if ( val && $inputText.length ) { try { @@ -36,6 +58,12 @@ }, initialize: function () { + // Native input: the browser provides the picker, we only keep the + // hidden input in sync with the value the input reports. + if ( this.$control().data( 'native' ) === 1 ) { + return this.initializeNative(); + } + // vars var $input = this.$input(); var $inputText = this.$inputText(); @@ -73,6 +101,14 @@ // action acf.doAction( 'time_picker_init', $inputText, args, this ); }, + + initializeNative: function () { + var self = this; + + this.$inputNative().on( 'change input', function () { + self.syncNative(); + } ); + }, } ); acf.registerFieldType( Field ); diff --git a/assets/src/sass/_fields.scss b/assets/src/sass/_fields.scss index 1fa5144b..6e94258c 100644 --- a/assets/src/sass/_fields.scss +++ b/assets/src/sass/_fields.scss @@ -632,6 +632,14 @@ html[dir="rtl"] input.acf-is-prepended.acf-is-appended { .acf-color-picker { + /* The native color input has an intrinsic size, keep it in line with the field row. */ + &.acf-native-picker input[type="color"] { + width: 60px; + height: 34px; + padding: 2px; + cursor: pointer; + } + .wp-color-result { border-color: $wp-input-border; diff --git a/docs/features/field/color-picker/index.md b/docs/features/field/color-picker/index.md index 924fa4c1..b3d9a52a 100644 --- a/docs/features/field/color-picker/index.md +++ b/docs/features/field/color-picker/index.md @@ -15,3 +15,7 @@ The Color Picker field provides an interactive interface for selecting colors. I - Default Value - Set a default color - Return Format - Choose between string, array, or rgba format - Enable Opacity - Allow transparency selection + +## Native Input + +When the native pickers beta feature is enabled, this field uses the browser's built-in color input for plain hex values. The stored value is unchanged. The browser input has no transparency, palette or color wheel support, so fields using Enable Transparency, a custom palette or a non-hex value keep the existing picker. diff --git a/docs/features/field/date-picker/index.md b/docs/features/field/date-picker/index.md index 92a48483..d7651b62 100644 --- a/docs/features/field/date-picker/index.md +++ b/docs/features/field/date-picker/index.md @@ -16,3 +16,7 @@ The Date Picker field provides a calendar interface for selecting dates. It offe - Return Format - How the date is stored/returned - Week Starts On - Set first day of week - First Day - Configure week start day + +## Native Input + +When the native pickers beta feature is enabled, this field uses the browser's built-in date input instead of the bundled calendar. The stored value is unchanged. The browser controls how the date is displayed, so the Display Format and Week Starts On settings no longer apply. Fields using a legacy Save Format keep the bundled calendar. diff --git a/docs/features/field/date-time-picker/index.md b/docs/features/field/date-time-picker/index.md index e0342936..0b641e23 100644 --- a/docs/features/field/date-time-picker/index.md +++ b/docs/features/field/date-time-picker/index.md @@ -16,3 +16,7 @@ The Date Time Picker field combines date and time selection into a single interf - Return Format - How the datetime is stored/returned - Week Starts On - Set first day of week - Time Increment - Control minute stepping + +## Native Input + +When the native pickers beta feature is enabled, this field uses the browser's built-in date and time inputs instead of the bundled picker. The stored value is unchanged. The browser controls how the value is displayed, so the Display Format and Week Starts On settings no longer apply. diff --git a/docs/features/field/time-picker/index.md b/docs/features/field/time-picker/index.md index 52d9e8c1..ff521379 100644 --- a/docs/features/field/time-picker/index.md +++ b/docs/features/field/time-picker/index.md @@ -17,3 +17,7 @@ The Time Picker field provides an interface for selecting time values with custo - Time Increment - Minute steps - Placeholder - Helper text - Default Value - Preset time + +## Native Input + +When the native pickers beta feature is enabled, this field uses the browser's built-in time input instead of the bundled picker. The stored value is unchanged. The browser controls how the time is displayed, so the Display Format setting no longer applies. diff --git a/includes/admin/beta-features.php b/includes/admin/beta-features.php index e0e656ea..98dffda0 100644 --- a/includes/admin/beta-features.php +++ b/includes/admin/beta-features.php @@ -137,8 +137,15 @@ public function admin_body_class( $classes ) { * @return void */ private function include_beta_features() { - acf_include( 'includes/admin/beta-features/class-scf-beta-feature.php' ); - acf_include( 'includes/admin/beta-features/class-scf-beta-feature-connect-fields.php' ); + // Only include the feature classes once, this method can run more than once per request. + static $included = false; + + if ( ! $included ) { + $included = true; + + acf_include( 'includes/admin/beta-features/class-scf-beta-feature.php' ); + acf_include( 'includes/admin/beta-features/class-scf-beta-feature-native-pickers.php' ); + } add_action( 'scf/include_admin_beta_features', array( $this, 'register_beta_features' ) ); @@ -153,6 +160,7 @@ private function include_beta_features() { * @return void */ public function register_beta_features() { + $this->register_beta_feature( 'SCF_Admin_Beta_Feature_Native_Pickers' ); } /** diff --git a/includes/admin/beta-features/class-scf-beta-feature-native-pickers.php b/includes/admin/beta-features/class-scf-beta-feature-native-pickers.php new file mode 100644 index 00000000..ff3beb9d --- /dev/null +++ b/includes/admin/beta-features/class-scf-beta-feature-native-pickers.php @@ -0,0 +1,41 @@ +name = 'native_pickers'; + $this->title = __( 'Native Date, Time and Color Inputs', 'secure-custom-fields' ); + $this->description = __( 'Uses the date, time and color inputs built into the browser instead of the bundled JavaScript pickers. The display format of date and time fields is then controlled by the browser. Fields using transparency, a custom color palette or a custom save format keep the existing pickers.', 'secure-custom-fields' ); + } + } +endif; diff --git a/includes/fields/class-acf-field-color_picker.php b/includes/fields/class-acf-field-color_picker.php index c740d4ce..b5a983c3 100644 --- a/includes/fields/class-acf-field-color_picker.php +++ b/includes/fields/class-acf-field-color_picker.php @@ -110,6 +110,13 @@ function input_admin_enqueue_scripts() { * @date 23/01/13 */ public function render_field( $field ) { + + // The browser's color input only supports an opaque hex value and has no palette support. + if ( $this->scf_use_native_picker() && $this->can_use_native_field( $field ) ) { + $this->render_native_field( $field ); + return; + } + $text_input = acf_get_sub_array( $field, array( 'id', 'class', 'name', 'value' ) ); $hidden_input = acf_get_sub_array( $field, array( 'name', 'value' ) ); $text_input['data-alpha-skip-debounce'] = true; @@ -175,6 +182,105 @@ public function render_field( $field ) { } + /** + * Checks whether the browser's color input can render this field. + * + * The native input only handles an opaque 6 digit hex value, it has no + * palette support and it cannot be cleared, so transparency, custom + * palettes and anything that is not a plain hex color keep the existing + * picker. + * + * @since SCF 6.9.6 + * + * @param array $field The field array. + * @return boolean + */ + protected function can_use_native_field( $field ) { + if ( ! empty( $field['enable_opacity'] ) ) { + return false; + } + + if ( ! empty( $field['custom_palette_source'] ) || ! empty( $field['palette_colors'] ) || ! empty( $field['show_custom_palette'] ) ) { + return false; + } + + // The native input always holds a color, so an empty value would be + // impossible to clear again. Keep the existing picker, which has one. + return null !== $this->to_native_hex( $field['value'] ); + } + + + /** + * Renders the field using the browser's native color input. + * + * The hidden input keeps the same value the existing picker would have + * saved, so stored data and return formats are unchanged. + * + * @since SCF 6.9.6 + * + * @param array $field The field array. + * @return void + */ + protected function render_native_field( $field ) { + $div = array( + 'class' => 'acf-color-picker acf-native-picker', + 'data-native' => '1', + ); + $hidden_input = array( + 'name' => $field['name'], + 'value' => $field['value'], + ); + $native_input = array( + 'id' => $field['id'], + 'class' => $field['class'] . ' input', + 'type' => 'color', + 'value' => $this->to_native_hex( $field['value'] ), + ); + + foreach ( array( 'readonly', 'disabled' ) as $k ) { + if ( ! empty( $field[ $k ] ) ) { + $hidden_input[ $k ] = $k; + $native_input[ $k ] = $k; + } + } + + ?> +
%s
', + esc_html__( 'The native date input is in use, so this format is only used by older picker fields. The browser decides how the date is displayed.', 'secure-custom-fields' ) + ); + } + // save_format - compatibility with ACF < 5.0.0 if ( ! empty( $field['save_format'] ) ) { acf_render_field_setting( @@ -229,6 +295,13 @@ function render_field_settings( $field ) { ) ); + if ( $native_picker ) { + printf( + '%s
', + esc_html__( 'The native date input is in use, so the browser decides which day the week starts on.', 'secure-custom-fields' ) + ); + } + acf_render_field_setting( $field, array( diff --git a/includes/fields/class-acf-field-date_time_picker.php b/includes/fields/class-acf-field-date_time_picker.php index 407cd916..214764cb 100644 --- a/includes/fields/class-acf-field-date_time_picker.php +++ b/includes/fields/class-acf-field-date_time_picker.php @@ -109,6 +109,12 @@ function render_field( $field ) { $display_value = acf_format_date( $field['value'], $field['display_format'] ); } + // The native date and time inputs use the ISO format and cannot express a custom display format. + if ( $this->scf_use_native_picker() ) { + $this->render_native_field( $field, $hidden_value ); + return; + } + // Convert "display_format" setting to individual date and time formats. $formats = acf_split_date_time( $field['display_format'] ); @@ -147,6 +153,61 @@ function render_field( $field ) { } + /** + * Renders the field using the browser's native date and time input. + * + * The hidden input keeps the `Y-m-d H:i:s` value the JavaScript picker + * would have saved, so stored data and return formats are unchanged. + * + * @since SCF 6.9.6 + * + * @param array $field The field array. + * @param string $hidden_value The value in the saved `Y-m-d H:i:s` format. + * @return void + */ + protected function render_native_field( $field, $hidden_value ) { + $native_value = $hidden_value ? acf_format_date( $hidden_value, 'Y-m-d\TH:i:s' ) : ''; + + // The JS picker applied the current date default, do the same here. + if ( '' === $native_value && ! empty( $field['default_to_current_date'] ) ) { + $native_value = current_time( 'Y-m-d\TH:i:s' ); + $hidden_value = current_time( 'Y-m-d H:i:s' ); + } + + $div = array( + 'class' => 'acf-date-time-picker acf-input-wrap acf-native-picker', + 'data-native' => '1', + ); + $hidden_input = array( + 'id' => $field['id'], + 'class' => 'input-alt', + 'name' => $field['name'], + 'value' => $hidden_value, + ); + $native_input = array( + 'class' => $field['class'] . ' input', + 'type' => 'datetime-local', + // Without a step the input hides seconds and reports Y-m-dTH:i, which is not the format we save. + 'step' => 1, + 'value' => $native_value, + ); + + foreach ( array( 'readonly', 'disabled' ) as $k ) { + if ( ! empty( $field[ $k ] ) ) { + $hidden_input[ $k ] = $k; + $native_input[ $k ] = $k; + } + } + + ?> +%s
', + esc_html__( 'The native date and time inputs are in use, so this format is only used by older picker fields. The browser decides how the value is displayed.', 'secure-custom-fields' ) + ); + } + acf_render_field_setting( $field, array( @@ -216,6 +286,13 @@ function render_field_settings( $field ) { ) ); + if ( $native_picker ) { + printf( + '%s
', + esc_html__( 'The native date and time inputs are in use, so the browser decides which day the week starts on.', 'secure-custom-fields' ) + ); + } + acf_render_field_setting( $field, array( diff --git a/includes/fields/class-acf-field-time_picker.php b/includes/fields/class-acf-field-time_picker.php index 0e37a037..708ccb2d 100644 --- a/includes/fields/class-acf-field-time_picker.php +++ b/includes/fields/class-acf-field-time_picker.php @@ -50,6 +50,12 @@ function render_field( $field ) { $display_value = acf_format_date( $field['value'], $field['display_format'] ); } + // The native time input uses the ISO format and cannot express a custom display format. + if ( $this->scf_use_native_picker() ) { + $this->render_native_field( $field ); + return; + } + // Elements. $div = array( 'class' => 'acf-time-picker acf-input-wrap', @@ -84,6 +90,58 @@ function render_field( $field ) { } + /** + * Renders the field using the browser's native time input. + * + * The hidden input keeps the same `H:i:s` value the JavaScript picker + * would have saved, so stored data and return formats are unchanged. + * + * @since SCF 6.9.6 + * + * @param array $field The field array. + * @return void + */ + protected function render_native_field( $field ) { + $native_value = ''; + + if ( ! empty( $field['value'] ) ) { + $native_value = acf_format_date( $field['value'], 'H:i:s' ); + } + + $div = array( + 'class' => 'acf-time-picker acf-input-wrap acf-native-picker', + 'data-native' => '1', + ); + $hidden_input = array( + 'id' => $field['id'], + 'class' => 'input-alt', + 'name' => $field['name'], + 'value' => $field['value'], + ); + $native_input = array( + 'class' => $field['class'] . ' input', + 'type' => 'time', + // Without a step the input hides seconds and reports HH:MM, which is not the H:i:s format we save. + 'step' => 1, + 'value' => $native_value, + ); + + foreach ( array( 'readonly', 'disabled' ) as $k ) { + if ( ! empty( $field[ $k ] ) ) { + $hidden_input[ $k ] = $k; + $native_input[ $k ] = $k; + } + } + + ?> +%s
', + esc_html__( 'The native time input is in use, so this format is only used by older picker fields. The browser decides how the time is displayed.', 'secure-custom-fields' ) + ); + } + acf_render_field_setting( $field, array( diff --git a/includes/fields/class-acf-field.php b/includes/fields/class-acf-field.php index c80eac95..8b1e010a 100644 --- a/includes/fields/class-acf-field.php +++ b/includes/fields/class-acf-field.php @@ -200,6 +200,31 @@ public function add_field_action( $tag = '', $function_to_add = '', $priority = $this->add_action( $tag, $function_to_add, $priority, $accepted_args ); } + /** + * Checks whether the native pickers beta feature is enabled. + * + * The beta feature registry is only loaded in the admin, so this falls + * back to the stored option when the registry is not available. That + * keeps front-end forms and REST requests consistent with the editor. + * + * @since SCF 6.9.6 + * + * @return boolean + */ + protected function scf_use_native_picker() { + $acf = function_exists( 'acf' ) ? acf() : null; + + if ( $acf && isset( $acf->admin_beta_features ) ) { + $feature = $acf->admin_beta_features->get_beta_feature( 'native_pickers' ); + + if ( $feature ) { + return $feature->is_enabled(); + } + } + + return (bool) get_option( 'scf_beta_feature_native_pickers_enabled', false ); + } + /** * Appends default settings to a field. * Runs on `acf/validate_field/type={$this->name}`. diff --git a/readme.txt b/readme.txt index 39f99d1f..bb4f8eaf 100644 --- a/readme.txt +++ b/readme.txt @@ -51,6 +51,13 @@ This plugin builds upon and is a fork of the previous work done by the contribut == Changelog == += 6.9.6 = +*Release Date TBD* + +*Enhancements* + +- Added a native pickers beta feature. When enabled, Date Picker, Date Time Picker and Time Picker fields use the browser's built-in date and time inputs, and Color Picker fields use the browser's color input when transparency, a custom palette or a non-hex value are not in use. The saved value format is unchanged, and fields that a native input cannot represent keep the existing pickers. + = 6.9.5 = *Release Date 7th August 2026* diff --git a/tests/e2e/fixtures.js b/tests/e2e/fixtures.js index 59d8b016..c8fee97e 100644 --- a/tests/e2e/fixtures.js +++ b/tests/e2e/fixtures.js @@ -144,22 +144,25 @@ const test = wpTest.extend( { 'role=button[name="View"i]' ); - if ( + const hasViewButton = ( await viewButton.count() ) > 0 && - ( await viewButton.first().isVisible() ) - ) { - return editor.openPreviewPage(); - } + ( await viewButton.first().isVisible() ); - await editorTopBar - .locator( 'role=button[name="Preview"i]' ) - .click(); + await ( hasViewButton + ? viewButton.first() + : editorTopBar.locator( 'role=button[name="Preview"i]' ) + ).click(); + // WordPress trunk renamed the menu item to + // "Preview (opens in a new tab)", so match on the shared + // "Preview ... new tab" text instead of the exact label. const [ previewPage ] = await Promise.all( [ context.waitForEvent( 'page' ), - page.click( - 'role=menuitem[name=/Preview in new tab/i]' - ), + page + .getByRole( 'menuitem', { + name: /Preview.*new tab/i, + } ) + .click(), ] ); return previewPage; diff --git a/tests/php/includes/fields/abstract-class-acf-field-test.php b/tests/php/includes/fields/abstract-class-acf-field-test.php index 4e30479f..e1fa9c7d 100644 --- a/tests/php/includes/fields/abstract-class-acf-field-test.php +++ b/tests/php/includes/fields/abstract-class-acf-field-test.php @@ -75,6 +75,37 @@ protected function get_field_include_path() { return "includes/fields/class-acf-field-{$filename}.php"; } + /** + * Render a field and return the generated HTML. + * + * The field type defaults are merged in first so the field matches what the + * render pipeline would pass after `validate_field` has run. + * + * @param array $field The field configuration. + * @return string The rendered HTML. + */ + protected function render_field_html( $field ) { + if ( isset( $this->field_instance->defaults ) && is_array( $this->field_instance->defaults ) ) { + $field = array_merge( $this->field_instance->defaults, $field ); + } + + $field['id'] = isset( $field['id'] ) ? $field['id'] : 'acf-field-test'; + $field['class'] = isset( $field['class'] ) ? $field['class'] : ''; + + ob_start(); + $this->field_instance->render_field( $field ); + return ob_get_clean(); + } + + /** + * Enable the native pickers beta feature for the current test. + * + * @return void + */ + protected function enable_native_pickers() { + update_option( 'scf_beta_feature_native_pickers_enabled', true ); + } + /** * Set up the test case. */ @@ -105,6 +136,8 @@ public function set_up() { * Clean up after each test. */ public function tear_down() { + delete_option( 'scf_beta_feature_native_pickers_enabled' ); + if ( $this->post_id ) { wp_delete_post( $this->post_id, true ); } diff --git a/tests/php/includes/fields/test-class-acf-field-color-picker.php b/tests/php/includes/fields/test-class-acf-field-color-picker.php index c931dceb..00e9fdde 100644 --- a/tests/php/includes/fields/test-class-acf-field-color-picker.php +++ b/tests/php/includes/fields/test-class-acf-field-color-picker.php @@ -123,4 +123,107 @@ public function test_format_value_rgba() { $this->assertEquals( $rgba, $result ); } + + /** + * Test the legacy picker markup is used by default. + */ + public function test_render_field_uses_legacy_picker_by_default() { + $field = $this->get_field( array( 'value' => '#ff5733' ) ); + + $html = $this->render_field_html( $field ); + + $this->assertStringNotContainsString( 'type="color"', $html ); + $this->assertStringContainsString( 'acf-color-picker', $html ); + } + + /** + * Test the native input is used for a plain hex value when enabled. + */ + public function test_render_field_uses_native_input_when_enabled() { + $this->enable_native_pickers(); + + $field = $this->get_field( array( 'value' => '#ff5733' ) ); + + $html = $this->render_field_html( $field ); + + $this->assertStringContainsString( 'type="color"', $html ); + $this->assertStringContainsString( 'value="#ff5733"', $html ); + // The named hidden input still carries the saved value. + $this->assertStringContainsString( 'name="test_color_picker"', $html ); + } + + /** + * Test a 3 digit hex value is expanded for the native input. + */ + public function test_native_input_expands_short_hex() { + $this->enable_native_pickers(); + + $field = $this->get_field( array( 'value' => '#F53' ) ); + + $html = $this->render_field_html( $field ); + + $this->assertStringContainsString( 'value="#ff5533"', $html ); + } + + /** + * Test opacity fields keep the existing picker. + */ + public function test_native_input_skipped_when_opacity_enabled() { + $this->enable_native_pickers(); + + $field = $this->get_field( + array( + 'value' => '#ff5733', + 'enable_opacity' => 1, + ) + ); + + $html = $this->render_field_html( $field ); + + $this->assertStringNotContainsString( 'type="color"', $html ); + } + + /** + * Test fields with a custom palette keep the existing picker. + */ + public function test_native_input_skipped_with_custom_palette() { + $this->enable_native_pickers(); + + $field = $this->get_field( + array( + 'value' => '#ff5733', + 'palette_colors' => '#ff0000,#00ff00', + ) + ); + + $html = $this->render_field_html( $field ); + + $this->assertStringNotContainsString( 'type="color"', $html ); + } + + /** + * Test a non hex value keeps the existing picker. + */ + public function test_native_input_skipped_for_non_hex_value() { + $this->enable_native_pickers(); + + $field = $this->get_field( array( 'value' => 'rgba(255, 87, 51, 0.5)' ) ); + + $html = $this->render_field_html( $field ); + + $this->assertStringNotContainsString( 'type="color"', $html ); + } + + /** + * Test an empty value keeps the existing picker. + */ + public function test_native_input_skipped_for_empty_value() { + $this->enable_native_pickers(); + + $field = $this->get_field( array( 'value' => '' ) ); + + $html = $this->render_field_html( $field ); + + $this->assertStringNotContainsString( 'type="color"', $html ); + } } diff --git a/tests/php/includes/fields/test-class-acf-field-date-picker.php b/tests/php/includes/fields/test-class-acf-field-date-picker.php index 3cf5fdc5..f93ae6ef 100644 --- a/tests/php/includes/fields/test-class-acf-field-date-picker.php +++ b/tests/php/includes/fields/test-class-acf-field-date-picker.php @@ -152,4 +152,75 @@ public function test_format_value_invalid_date() { // Invalid dates should still return something. $this->assertNotNull( $result ); } + + /** + * Test the legacy picker markup is used by default. + */ + public function test_render_field_uses_legacy_picker_by_default() { + $field = $this->get_field( array( 'value' => '20231225' ) ); + $field['id'] = 'acf-field-date'; + $field['class'] = ''; + + $html = $this->render_field_html( $field ); + + $this->assertStringNotContainsString( 'type="date"', $html ); + $this->assertStringContainsString( 'acf-date-picker', $html ); + } + + /** + * Test the native input is used when the beta feature is enabled. + */ + public function test_render_field_uses_native_input_when_enabled() { + $this->enable_native_pickers(); + + $field = $this->get_field( array( 'value' => '20231225' ) ); + $field['id'] = 'acf-field-date'; + $field['class'] = ''; + + $html = $this->render_field_html( $field ); + + $this->assertStringContainsString( 'type="date"', $html ); + $this->assertStringContainsString( 'value="2023-12-25"', $html ); + // The named hidden input keeps the stored Ymd format. + $this->assertStringContainsString( 'name="test_date_picker"', $html ); + $this->assertStringContainsString( 'value="20231225"', $html ); + } + + /** + * Test the saved value format is unchanged in native mode. + */ + public function test_native_input_keeps_stored_format() { + $this->enable_native_pickers(); + + $field = $this->get_field( array( 'value' => '20231225' ) ); + $field['id'] = 'acf-field-date'; + $field['class'] = ''; + + $html = $this->render_field_html( $field ); + + $this->assertStringContainsString( 'value="2023-12-25"', $html ); + $this->assertStringContainsString( 'name="test_date_picker"', $html ); + $this->assertStringContainsString( 'value="20231225"', $html ); + } + + /** + * Test a legacy save_format field keeps the JavaScript picker in native mode. + */ + public function test_render_field_keeps_legacy_markup_with_save_format() { + $this->enable_native_pickers(); + + $field = $this->get_field( + array( + 'value' => '2023-12-25', + 'save_format' => 'Y-m-d', + ) + ); + $field['id'] = 'acf-field-date'; + $field['class'] = ''; + + $html = $this->render_field_html( $field ); + + $this->assertStringNotContainsString( 'type="date"', $html ); + $this->assertStringContainsString( 'data-save_format', $html ); + } } diff --git a/tests/php/includes/fields/test-class-acf-field-date-time-picker.php b/tests/php/includes/fields/test-class-acf-field-date-time-picker.php index 948c3c5c..98614c1d 100644 --- a/tests/php/includes/fields/test-class-acf-field-date-time-picker.php +++ b/tests/php/includes/fields/test-class-acf-field-date-time-picker.php @@ -160,4 +160,34 @@ public function test_format_value_midnight() { $this->assertEquals( '2023-12-25 00:00:00', $result ); } + + /** + * Test the legacy picker markup is used by default. + */ + public function test_render_field_uses_legacy_picker_by_default() { + $field = $this->get_field( array( 'value' => '2023-12-25 14:30:00' ) ); + + $html = $this->render_field_html( $field ); + + $this->assertStringNotContainsString( 'type="datetime-local"', $html ); + $this->assertStringContainsString( 'acf-date-time-picker', $html ); + } + + /** + * Test the native input is used when the beta feature is enabled. + */ + public function test_render_field_uses_native_input_when_enabled() { + $this->enable_native_pickers(); + + $field = $this->get_field( array( 'value' => '2023-12-25 14:30:00' ) ); + + $html = $this->render_field_html( $field ); + + $this->assertStringContainsString( 'type="datetime-local"', $html ); + $this->assertStringContainsString( 'value="2023-12-25T14:30:00"', $html ); + // The named hidden input keeps the stored Y-m-d H:i:s format. + $this->assertStringContainsString( 'name="test_date_time_picker"', $html ); + $this->assertStringContainsString( 'name="test_date_time_picker"', $html ); + $this->assertStringContainsString( 'value="2023-12-25 14:30:00"', $html ); + } } diff --git a/tests/php/includes/fields/test-class-acf-field-time-picker.php b/tests/php/includes/fields/test-class-acf-field-time-picker.php index 62dc3fb2..d7985f23 100644 --- a/tests/php/includes/fields/test-class-acf-field-time-picker.php +++ b/tests/php/includes/fields/test-class-acf-field-time-picker.php @@ -160,4 +160,34 @@ public function test_format_value_end_of_day() { $this->assertEquals( '23:59:59', $result ); } + + /** + * Test the legacy picker markup is used by default. + */ + public function test_render_field_uses_legacy_picker_by_default() { + $field = $this->get_field( array( 'value' => '14:30:00' ) ); + + $html = $this->render_field_html( $field ); + + $this->assertStringNotContainsString( 'type="time"', $html ); + $this->assertStringContainsString( 'acf-time-picker', $html ); + } + + /** + * Test the native input is used when the beta feature is enabled. + */ + public function test_render_field_uses_native_input_when_enabled() { + $this->enable_native_pickers(); + + $field = $this->get_field( array( 'value' => '14:30:00' ) ); + + $html = $this->render_field_html( $field ); + + $this->assertStringContainsString( 'type="time"', $html ); + $this->assertStringContainsString( 'value="14:30:00"', $html ); + // The named hidden input keeps the stored H:i:s format. + $this->assertStringContainsString( 'name="test_time_picker"', $html ); + $this->assertStringContainsString( 'name="test_time_picker"', $html ); + $this->assertStringContainsString( 'value="14:30:00"', $html ); + } } diff --git a/tests/php/test-admin-beta-features.php b/tests/php/test-admin-beta-features.php index 2d0a971a..02753be5 100644 --- a/tests/php/test-admin-beta-features.php +++ b/tests/php/test-admin-beta-features.php @@ -29,6 +29,7 @@ public function set_up() { acf_include( 'includes/admin/beta-features.php' ); acf_include( 'includes/admin/beta-features/class-scf-beta-feature.php' ); acf_include( 'includes/admin/beta-features/class-scf-beta-feature-editor-sidebar.php' ); + acf_include( 'includes/admin/beta-features/class-scf-beta-feature-native-pickers.php' ); $this->beta_features = new SCF_Admin_Beta_Features(); } @@ -38,6 +39,7 @@ public function set_up() { */ public function tear_down() { delete_option( 'scf_beta_feature_editor_sidebar_enabled' ); + delete_option( 'scf_beta_feature_native_pickers_enabled' ); parent::tear_down(); } @@ -152,4 +154,38 @@ public function test_beta_feature_nonce_verification() { $this->assertFalse( $beta_feature->is_enabled() ); $this->assertFalse( get_option( 'scf_beta_feature_editor_sidebar_enabled' ) ); } + + /** + * Test the native pickers feature is registered by default. + */ + public function test_native_pickers_registered_by_default() { + $this->beta_features->register_beta_features(); + + $beta_feature = $this->beta_features->get_beta_feature( 'native_pickers' ); + + $this->assertNotNull( $beta_feature ); + $this->assertInstanceOf( 'SCF_Admin_Beta_Feature_Native_Pickers', $beta_feature ); + $this->assertEquals( 'native_pickers', $beta_feature->name ); + $this->assertNotEmpty( $beta_feature->title ); + $this->assertNotEmpty( $beta_feature->description ); + } + + /** + * Test the native pickers feature is disabled by default and can be toggled. + */ + public function test_native_pickers_enable_disable() { + $this->beta_features->register_beta_features(); + + $beta_feature = $this->beta_features->get_beta_feature( 'native_pickers' ); + + $this->assertFalse( $beta_feature->is_enabled() ); + + $beta_feature->set_enabled( true ); + $this->assertTrue( $beta_feature->is_enabled() ); + $this->assertTrue( get_option( 'scf_beta_feature_native_pickers_enabled' ) ); + + $beta_feature->set_enabled( false ); + $this->assertFalse( $beta_feature->is_enabled() ); + $this->assertFalse( get_option( 'scf_beta_feature_native_pickers_enabled' ) ); + } }