Skip to content

Button Group field type #16782

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/fields/BaseOptionsField.php
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,10 @@ public function getContentGqlMutationArgumentType(): Type|array
*
* @return string
*/
abstract protected function optionsSettingLabel(): string;
protected function optionsSettingLabel(): string
{
return Craft::t('app', 'Options');
}

/**
* Returns the available options (and optgroups) for the field.
Expand Down
168 changes: 168 additions & 0 deletions src/fields/ButtonGroup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\fields;

use Craft;
use craft\base\ElementInterface;
use craft\base\SortableFieldInterface;
use craft\fields\data\SingleOptionFieldData;
use craft\helpers\Cp;
use craft\helpers\Html;

/**
* RadioButtons represents a Radio Buttons field.
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 3.0.0
*/
class ButtonGroup extends BaseOptionsField implements SortableFieldInterface
{
/**
* @inheritdoc
*/
protected static bool $optionIcons = true;

/**
* @inheritdoc
*/
public static function displayName(): string
{
return Craft::t('app', 'Button Group');
}

/**
* @inheritdoc
*/
public static function icon(): string
{
return 'hand-pointer';
}

/**
* @var bool Whether buttons should only show their icons, hiding their text labels
*/
public bool $iconsOnly = false;

/**
* @inheritdoc
*/
public function getSettingsHtml(): ?string
{
return parent::getSettingsHtml() .
Cp::lightswitchFieldHtml([
'label' => Craft::t('app', 'Icons only'),
'instructions' => Craft::t('app', 'Whether buttons should only show their icons, hiding their text labels.'),
'name' => 'iconsOnly',
'on' => $this->iconsOnly,
]);
}

/**
* @inheritdoc
*/
public function useFieldset(): bool
{
return true;
}

/**
* @inheritdoc
*/
protected function inputHtml(mixed $value, ?ElementInterface $element, bool $inline): string
{
return $this->_inputHtml($value, $element, false);
}

/**
* @inheritdoc
*/
public function getStaticHtml(mixed $value, ElementInterface $element): string
{
return $this->_inputHtml($value, $element, true);
}

private function _inputHtml(SingleOptionFieldData $value, ?ElementInterface $element, bool $static): string
{
/** @var SingleOptionFieldData $value */
if (!$value->valid) {
Craft::$app->getView()->setInitialDeltaValue($this->handle, null);
}

$id = $this->getInputId();

$html = Html::beginTag('section', [
'id' => $id,
'class' => ['btngroup', 'btngroup--exclusive'],
'aria' => [
'labelledby' => $this->getLabelId(),
],
]);

$value = $this->encodeValue($value);

foreach ($this->translatedOptions(true, $value, $element) as $option) {
$selected = $option['value'] === $value;

if ($this->iconsOnly && !empty($option['icon'])) {
$labelHtml = Html::tag('div', Cp::iconSvg($option['icon']), [
'class' => 'cp-icon',
'aria' => [
'label' => $option['label'],
],
]);
} else {
$labelHtml = Html::encode($option['label']);
if (!empty($option['icon'])) {
$labelHtml = Html::beginTag('div', ['class' => ['flex', 'flex-inline', 'gap-xs']]) .
Html::tag('div', Cp::iconSvg($option['icon']), [
'class' => 'cp-icon',
]) .
Html::tag('div', $labelHtml) .
Html::endTag('div');
}
}

$html .= Cp::buttonHtml([
'labelHtml' => $labelHtml,
'type' => 'button',
'class' => array_filter([
$selected ? 'active' : null,
]),
'disabled' => $static,
'attributes' => [
'aria' => [
'pressed' => $selected ? 'true' : false,
],
'data' => [
'value' => $option['value'],
],
],
]);
}

$html .= Html::endTag('section') . // .btngroup
Html::hiddenInput($this->handle, $value, [
'id' => "{$id}-input",
]);

$view = Craft::$app->getView();
$view->registerJsWithVars(fn($id) => <<<JS
(() => {
new Craft.Listbox($('#' + $id), {
onChange: (selectedOption) => {
$('#' + $id + '-input').val(selectedOption.data('value'));
},
});
})();
JS, [
$view->namespaceInputId($id),
]);

return $html;
}
}
13 changes: 13 additions & 0 deletions src/helpers/Cp.php
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,19 @@ private static function _noticeHtml(string $id, string $class, string $label, ?s
Html::endTag('p');
}

/**
* Renders a button’s HTML.
*
* @param array $config
* @return string
* @throws InvalidArgumentException
* @since 5.7.0
*/
public static function buttonHtml(array $config): string
{
return static::renderTemplate('_includes/forms/button.twig', $config);
}

/**
* Renders a checkbox field’s HTML.
*
Expand Down
2 changes: 2 additions & 0 deletions src/services/Fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use craft\fields\Addresses as AddressesField;
use craft\fields\Assets as AssetsField;
use craft\fields\BaseRelationField;
use craft\fields\ButtonGroup;
use craft\fields\Categories as CategoriesField;
use craft\fields\Checkboxes;
use craft\fields\Color;
Expand Down Expand Up @@ -222,6 +223,7 @@ public function getAllFieldTypes(): array
$fieldTypes = [
AddressesField::class,
AssetsField::class,
ButtonGroup::class,
CategoriesField::class,
Checkboxes::class,
Color::class,
Expand Down
1 change: 1 addition & 0 deletions src/templates/_includes/forms/button.twig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
class: (class ?? [])|explodeClass|merge([
'btn',
not (label or labelHtml) ? 'btn-empty' : null,
(disabled ?? false) ? 'disabled',
]|filter),
data: {
'busy-message': busyMessage,
Expand Down
3 changes: 3 additions & 0 deletions src/translations/en/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@
'Briefly describe your issue or idea.' => 'Briefly describe your issue or idea.',
'Briefly describe your question.' => 'Briefly describe your question.',
'Bug reports and feature requests' => 'Bug reports and feature requests',
'Button Group' => 'Button Group',
'Buy now' => 'Buy now',
'Buy {name}' => 'Buy {name}',
'Bytes' => 'Bytes',
Expand Down Expand Up @@ -815,6 +816,7 @@
'How-to’s and other questions' => 'How-to’s and other questions',
'ID' => 'ID',
'Icon' => 'Icon',
'Icons only' => 'Icons only',
'If the URI looks like this' => 'If the URI looks like this',
'Image Format' => 'Image Format',
'Image Height' => 'Image Height',
Expand Down Expand Up @@ -1985,6 +1987,7 @@
'Where transforms should be stored on the filesystem.' => 'Where transforms should be stored on the filesystem.',
'Whether authors should be able to choose which time zone the time is in.' => 'Whether authors should be able to choose which time zone the time is in.',
'Whether authors should be able to upload files directly to the field, rather than requiring them to select/upload assets via the selection modal.' => 'Whether authors should be able to upload files directly to the field, rather than requiring them to select/upload assets via the selection modal.',
'Whether buttons should only show their icons, hiding their text labels.' => 'Whether buttons should only show their icons, hiding their text labels.',
'Whether cards should be shown in a multi-column grid on wide viewports.' => 'Whether cards should be shown in a multi-column grid on wide viewports.',
'Whether custom fields should be validated during public registration.' => 'Whether custom fields should be validated during public registration.',
'Whether empty folders should be listed for deletion.' => 'Whether empty folders should be listed for deletion.',
Expand Down