mirror of
https://github.com/flarum/core.git
synced 2025-07-25 18:51:40 +02:00
feat: remove color validation in appearance admin page & add color indicator (#3140)
* Remove color validation in basics admin page & add color indicator * Create ColorInput common component * Revert 'formGroupAttrs' addition * Rename component CSS classes * Fix input type in ColorInput from AdminPage#buildSettingComponent * Rename component to ColorPreviewInput, remove aliases in admin & export in compat * Remove leftovers from rebase on master * feat: add global type definition for a vnode element tag * fix(a11y): add aria roles to color input * chore: use new type * chore: format Co-authored-by: David Wheatley <hi@davwheat.dev>
This commit is contained in:
committed by
GitHub
parent
c96fa49853
commit
94c4f266e3
2
js/src/@types/global.d.ts
vendored
2
js/src/@types/global.d.ts
vendored
@@ -21,6 +21,8 @@ declare type KeysOfType<Type extends object, Match> = {
|
|||||||
*/
|
*/
|
||||||
declare type KeyOfType<Type extends object, Match> = KeysOfType<Type, Match>[keyof Type];
|
declare type KeyOfType<Type extends object, Match> = KeysOfType<Type, Match>[keyof Type];
|
||||||
|
|
||||||
|
declare type VnodeElementTag<Attrs = Record<string, unknown>, State = Record<string, unknown>> = string | ComponentTypes<Attrs, State>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Please import `app` from a namespace instead of using it as a global variable.
|
* @deprecated Please import `app` from a namespace instead of using it as a global variable.
|
||||||
*
|
*
|
||||||
|
@@ -10,6 +10,7 @@ import Stream from '../../common/utils/Stream';
|
|||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
import AdminHeader from './AdminHeader';
|
import AdminHeader from './AdminHeader';
|
||||||
import generateElementId from '../utils/generateElementId';
|
import generateElementId from '../utils/generateElementId';
|
||||||
|
import ColorPreviewInput from '../../common/components/ColorPreviewInput';
|
||||||
|
|
||||||
export interface AdminHeaderOptions {
|
export interface AdminHeaderOptions {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -76,6 +77,7 @@ export interface HTMLInputSettingsComponentOptions extends CommonSettingsItemOpt
|
|||||||
const BooleanSettingTypes = ['bool', 'checkbox', 'switch', 'boolean'] as const;
|
const BooleanSettingTypes = ['bool', 'checkbox', 'switch', 'boolean'] as const;
|
||||||
const SelectSettingTypes = ['select', 'dropdown', 'selectdropdown'] as const;
|
const SelectSettingTypes = ['select', 'dropdown', 'selectdropdown'] as const;
|
||||||
const TextareaSettingTypes = ['textarea'] as const;
|
const TextareaSettingTypes = ['textarea'] as const;
|
||||||
|
const ColorPreviewSettingType = 'color-preview';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid options for the setting component builder to generate a Switch.
|
* Valid options for the setting component builder to generate a Switch.
|
||||||
@@ -103,6 +105,13 @@ export interface TextareaSettingComponentOptions extends CommonSettingsItemOptio
|
|||||||
type: typeof TextareaSettingTypes[number];
|
type: typeof TextareaSettingTypes[number];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid options for the setting component builder to generate a ColorPreviewInput.
|
||||||
|
*/
|
||||||
|
export interface ColorPreviewSettingComponentOptions extends CommonSettingsItemOptions {
|
||||||
|
type: typeof ColorPreviewSettingType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All valid options for the setting component builder.
|
* All valid options for the setting component builder.
|
||||||
*/
|
*/
|
||||||
@@ -110,7 +119,8 @@ export type SettingsComponentOptions =
|
|||||||
| HTMLInputSettingsComponentOptions
|
| HTMLInputSettingsComponentOptions
|
||||||
| SwitchSettingComponentOptions
|
| SwitchSettingComponentOptions
|
||||||
| SelectSettingComponentOptions
|
| SelectSettingComponentOptions
|
||||||
| TextareaSettingComponentOptions;
|
| TextareaSettingComponentOptions
|
||||||
|
| ColorPreviewSettingComponentOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid attrs that can be returned by the `headerInfo` function
|
* Valid attrs that can be returned by the `headerInfo` function
|
||||||
@@ -258,7 +268,15 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
|
|||||||
if ((TextareaSettingTypes as readonly string[]).includes(type)) {
|
if ((TextareaSettingTypes as readonly string[]).includes(type)) {
|
||||||
settingElement = <textarea id={inputId} aria-describedby={helpTextId} bidi={this.setting(setting)} {...componentAttrs} />;
|
settingElement = <textarea id={inputId} aria-describedby={helpTextId} bidi={this.setting(setting)} {...componentAttrs} />;
|
||||||
} else {
|
} else {
|
||||||
settingElement = <input id={inputId} aria-describedby={helpTextId} type={type} bidi={this.setting(setting)} {...componentAttrs} />;
|
let Tag: VnodeElementTag = 'input';
|
||||||
|
|
||||||
|
if (type === ColorPreviewSettingType) {
|
||||||
|
Tag = ColorPreviewInput;
|
||||||
|
} else {
|
||||||
|
componentAttrs.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingElement = <Tag id={inputId} aria-describedby={helpTextId} bidi={this.setting(setting)} {...componentAttrs} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,12 +25,12 @@ export default class AppearancePage extends AdminPage {
|
|||||||
|
|
||||||
<div className="AppearancePage-colors-input">
|
<div className="AppearancePage-colors-input">
|
||||||
{this.buildSettingComponent({
|
{this.buildSettingComponent({
|
||||||
type: 'text',
|
type: 'color-preview',
|
||||||
setting: 'theme_primary_color',
|
setting: 'theme_primary_color',
|
||||||
placeholder: '#aaaaaa',
|
placeholder: '#aaaaaa',
|
||||||
})}
|
})}
|
||||||
{this.buildSettingComponent({
|
{this.buildSettingComponent({
|
||||||
type: 'text',
|
type: 'color-preview',
|
||||||
setting: 'theme_secondary_color',
|
setting: 'theme_secondary_color',
|
||||||
placeholder: '#aaaaaa',
|
placeholder: '#aaaaaa',
|
||||||
})}
|
})}
|
||||||
@@ -105,17 +105,4 @@ export default class AppearancePage extends AdminPage {
|
|||||||
onsaved() {
|
onsaved() {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const hex = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
|
|
||||||
|
|
||||||
if (!hex.test(this.settings['theme_primary_color']()) || !hex.test(this.settings['theme_secondary_color']())) {
|
|
||||||
alert(app.translator.trans('core.admin.appearance.enter_hex_message'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.saveSettings(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -57,6 +57,7 @@ import Alert from './components/Alert';
|
|||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
import LinkButton from './components/LinkButton';
|
import LinkButton from './components/LinkButton';
|
||||||
import Checkbox from './components/Checkbox';
|
import Checkbox from './components/Checkbox';
|
||||||
|
import ColorPreviewInput from './components/ColorPreviewInput';
|
||||||
import SelectDropdown from './components/SelectDropdown';
|
import SelectDropdown from './components/SelectDropdown';
|
||||||
import ModalManager from './components/ModalManager';
|
import ModalManager from './components/ModalManager';
|
||||||
import Button from './components/Button';
|
import Button from './components/Button';
|
||||||
@@ -144,6 +145,7 @@ export default {
|
|||||||
'components/Link': Link,
|
'components/Link': Link,
|
||||||
'components/LinkButton': LinkButton,
|
'components/LinkButton': LinkButton,
|
||||||
'components/Checkbox': Checkbox,
|
'components/Checkbox': Checkbox,
|
||||||
|
'components/ColorPreviewInput': ColorPreviewInput,
|
||||||
'components/SelectDropdown': SelectDropdown,
|
'components/SelectDropdown': SelectDropdown,
|
||||||
'components/ModalManager': ModalManager,
|
'components/ModalManager': ModalManager,
|
||||||
'components/Button': Button,
|
'components/Button': Button,
|
||||||
|
28
js/src/common/components/ColorPreviewInput.tsx
Normal file
28
js/src/common/components/ColorPreviewInput.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type Mithril from 'mithril';
|
||||||
|
|
||||||
|
import Component, { ComponentAttrs } from '../Component';
|
||||||
|
import classList from '../utils/classList';
|
||||||
|
import icon from '../helpers/icon';
|
||||||
|
|
||||||
|
export default class ColorPreviewInput extends Component {
|
||||||
|
value?: string;
|
||||||
|
|
||||||
|
view(vnode: Mithril.Vnode<ComponentAttrs, this>) {
|
||||||
|
const { className, ...attrs } = this.attrs;
|
||||||
|
const value = attrs.bidi?.() || attrs.value;
|
||||||
|
|
||||||
|
attrs.type ||= 'text';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ColorInput">
|
||||||
|
<input className={classList('FormControl', className)} {...attrs} />
|
||||||
|
|
||||||
|
<span className="ColorInput-icon" role="presentation">
|
||||||
|
{icon('fas fa-exclamation-circle')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="ColorInput-preview" style={{ '--input-value': value }} role="presentation" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -23,14 +23,6 @@
|
|||||||
margin-bottom: 24px !important;
|
margin-bottom: 24px !important;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
|
||||||
float: left;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-right: 2%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.AppearancePage-colors .Checkbox {
|
.AppearancePage-colors .Checkbox {
|
||||||
|
22
less/common/ColorInput.less
Normal file
22
less/common/ColorInput.less
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.ColorInput {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&-preview, &-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-preview {
|
||||||
|
background-color: var(--input-value);
|
||||||
|
border-radius: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
text-align: center;
|
||||||
|
color: @validation-error-color;
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,7 @@
|
|||||||
@import "Badge";
|
@import "Badge";
|
||||||
@import "Button";
|
@import "Button";
|
||||||
@import "Checkbox";
|
@import "Checkbox";
|
||||||
|
@import "ColorInput";
|
||||||
@import "Dropdown";
|
@import "Dropdown";
|
||||||
@import "EditUserModal";
|
@import "EditUserModal";
|
||||||
@import "Form";
|
@import "Form";
|
||||||
|
Reference in New Issue
Block a user