From 55121797fcef5cc1a1ae55aba09f7699be4b4dda Mon Sep 17 00:00:00 2001 From: Beau Hastings Date: Tue, 18 Nov 2025 13:05:31 -0600 Subject: [PATCH] [5.x] Support decimal values in Range fieldtype The Range fieldtype now properly supports decimal values (e.g., 0.5, 3.14) to align with the HTML range input spec, which allows the step attribute to accept decimal values. Changes: - Config fields (min, max, step) now accept decimal values - process() method returns int or float based on field configuration - If min, max, or step contains decimals, returns float - If all config values are integers, returns int (backwards compatible) - GraphQL type dynamically determined based on field configuration This maintains full backwards compatibility: existing Range fields with integer configurations continue to return integers and use Int GraphQL type. Only fields explicitly configured with decimal values will use Float type. --- src/Fieldtypes/Range.php | 33 ++++- .../GraphQL/Fieldtypes/RangeFieldtypeTest.php | 26 +++- tests/Fieldtypes/RangeFieldtypeTest.php | 137 ++++++++++++++++++ 3 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 tests/Fieldtypes/RangeFieldtypeTest.php diff --git a/src/Fieldtypes/Range.php b/src/Fieldtypes/Range.php index a31482f6bc8..a15b0708423 100644 --- a/src/Fieldtypes/Range.php +++ b/src/Fieldtypes/Range.php @@ -18,19 +18,22 @@ protected function configFieldItems(): array 'min' => [ 'display' => __('Min'), 'instructions' => __('statamic::fieldtypes.range.config.min'), - 'type' => 'integer', + 'type' => 'text', + 'input_type' => 'number', 'default' => 0, ], 'max' => [ 'display' => __('Max'), 'instructions' => __('statamic::fieldtypes.range.config.max'), - 'type' => 'integer', + 'type' => 'text', + 'input_type' => 'number', 'default' => 100, ], 'step' => [ 'display' => __('Step'), 'instructions' => __('statamic::fieldtypes.range.config.step'), - 'type' => 'integer', + 'type' => 'text', + 'input_type' => 'number', 'default' => 1, ], 'default' => [ @@ -62,11 +65,33 @@ protected function configFieldItems(): array public function process($data) { + if ($this->usesDecimals()) { + return (float) $data; + } + return (int) $data; } + protected function usesDecimals(): bool + { + $step = $this->config('step', 1); + $min = $this->config('min', 0); + $max = $this->config('max', 100); + + return $this->isDecimal($step) || $this->isDecimal($min) || $this->isDecimal($max); + } + + protected function isDecimal($value): bool + { + if (! is_numeric($value)) { + return false; + } + + return floor((float) $value) != (float) $value; + } + public function toGqlType() { - return GraphQL::int(); + return $this->usesDecimals() ? GraphQL::float() : GraphQL::int(); } } diff --git a/tests/Feature/GraphQL/Fieldtypes/RangeFieldtypeTest.php b/tests/Feature/GraphQL/Fieldtypes/RangeFieldtypeTest.php index 9ee651bdb5f..dbb9f65e75f 100644 --- a/tests/Feature/GraphQL/Fieldtypes/RangeFieldtypeTest.php +++ b/tests/Feature/GraphQL/Fieldtypes/RangeFieldtypeTest.php @@ -9,16 +9,16 @@ class RangeFieldtypeTest extends FieldtypeTestCase { #[Test] - public function it_gets_an_integer() + public function it_processes_integer_values_with_integer_config() { $this->createEntryWithFields([ 'filled' => [ 'value' => '7', - 'field' => ['type' => 'range'], + 'field' => ['type' => 'range', 'min' => 0, 'max' => 100, 'step' => 1], ], 'undefined' => [ 'value' => null, - 'field' => ['type' => 'range'], + 'field' => ['type' => 'range', 'min' => 0, 'max' => 100, 'step' => 1], ], ]); @@ -27,4 +27,24 @@ public function it_gets_an_integer() 'undefined' => null, ]); } + + #[Test] + public function it_processes_decimal_values_with_decimal_config() + { + $this->createEntryWithFields([ + 'filled' => [ + 'value' => '7.5', + 'field' => ['type' => 'range', 'min' => 0, 'max' => 100, 'step' => 0.1], + ], + 'another' => [ + 'value' => '3.14', + 'field' => ['type' => 'range', 'min' => 0, 'max' => 10, 'step' => 0.01], + ], + ]); + + $this->assertGqlEntryHas('filled, another', [ + 'filled' => 7.5, + 'another' => 3.14, + ]); + } } diff --git a/tests/Fieldtypes/RangeFieldtypeTest.php b/tests/Fieldtypes/RangeFieldtypeTest.php new file mode 100644 index 00000000000..186a48f5089 --- /dev/null +++ b/tests/Fieldtypes/RangeFieldtypeTest.php @@ -0,0 +1,137 @@ +setField(new Field('test', [ + 'type' => 'range', + 'min' => 0, + 'max' => 100, + 'step' => 1, + ])); + + $result = $fieldtype->process('7'); + + $this->assertIsInt($result); + $this->assertEquals(7, $result); + } + + #[Test] + public function it_processes_as_float_with_decimal_step() + { + $fieldtype = (new Range())->setField(new Field('test', [ + 'type' => 'range', + 'min' => 0, + 'max' => 100, + 'step' => 0.1, + ])); + + $result = $fieldtype->process('7.5'); + + $this->assertIsFloat($result); + $this->assertEquals(7.5, $result); + } + + #[Test] + public function it_processes_as_float_with_decimal_min() + { + $fieldtype = (new Range())->setField(new Field('test', [ + 'type' => 'range', + 'min' => 0.5, + 'max' => 100, + 'step' => 1, + ])); + + $result = $fieldtype->process('7'); + + $this->assertIsFloat($result); + $this->assertEquals(7.0, $result); + } + + #[Test] + public function it_processes_as_float_with_decimal_max() + { + $fieldtype = (new Range())->setField(new Field('test', [ + 'type' => 'range', + 'min' => 0, + 'max' => 99.9, + 'step' => 1, + ])); + + $result = $fieldtype->process('7'); + + $this->assertIsFloat($result); + $this->assertEquals(7.0, $result); + } + + #[Test] + public function it_processes_zero_as_integer_with_integer_config() + { + $fieldtype = (new Range())->setField(new Field('test', [ + 'type' => 'range', + 'min' => 0, + 'max' => 100, + 'step' => 1, + ])); + + $result = $fieldtype->process('0'); + + $this->assertIsInt($result); + $this->assertEquals(0, $result); + } + + #[Test] + public function it_processes_negative_values_with_decimal_step() + { + $fieldtype = (new Range())->setField(new Field('test', [ + 'type' => 'range', + 'min' => -10, + 'max' => 10, + 'step' => 0.5, + ])); + + $result = $fieldtype->process('-2.5'); + + $this->assertIsFloat($result); + $this->assertEquals(-2.5, $result); + } + + #[Test] + public function it_returns_int_graphql_type_with_integer_config() + { + $fieldtype = (new Range())->setField(new Field('test', [ + 'type' => 'range', + 'min' => 0, + 'max' => 100, + 'step' => 1, + ])); + + $type = $fieldtype->toGqlType(); + + $this->assertEquals('Int', $type->name); + } + + #[Test] + public function it_returns_float_graphql_type_with_decimal_config() + { + $fieldtype = (new Range())->setField(new Field('test', [ + 'type' => 'range', + 'min' => 0, + 'max' => 100, + 'step' => 0.1, + ])); + + $type = $fieldtype->toGqlType(); + + $this->assertEquals('Float', $type->name); + } +}