diff --git a/ivette/.eslintignore b/ivette/.eslintignore index da6324fae8fd497e4a06a672c5e08272d520ec22..e0d530b07981ceaa194fcffe211a324d309dc00e 100644 --- a/ivette/.eslintignore +++ b/ivette/.eslintignore @@ -9,7 +9,6 @@ lib # don't lint the generated API src/api # lint Dome step by step -src/dome/src/renderer/controls src/dome/src/renderer/layout src/dome/src/renderer/table src/dome/src/renderer/data diff --git a/ivette/.eslintrc.js b/ivette/.eslintrc.js index a06e294a3f4d8da672398dd03d0225f6342b0804..1d1eaf1486b532b7221407287a5350c65b4823a1 100644 --- a/ivette/.eslintrc.js +++ b/ivette/.eslintrc.js @@ -81,5 +81,13 @@ module.exports = { "react/destructuring-assignment": "off", // Allow console errors and warnings "no-console": ["error", { allow: ["warn", "error"] }], + // Disable accessibility rules + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/no-static-element-interactions": "off", + "jsx-a11y/no-noninteractive-element-interactions": "off", + "jsx-a11y/no-autofocus": "off", + // Completely broken rule + "react/prop-types": "off", } }; diff --git a/ivette/src/dome/src/misc/utils.ts b/ivette/src/dome/src/misc/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..808f425901a297da27a7e4576662fe388cd2cb9a --- /dev/null +++ b/ivette/src/dome/src/misc/utils.ts @@ -0,0 +1,29 @@ +// -------------------------------------------------------------------------- +// --- Utilities +// -------------------------------------------------------------------------- + +type specClass = + undefined | boolean | null | string | + { [cname: string]: boolean | null | undefined }; + +export function classes( + ...args: specClass[] +): string { + const buffer: string[] = []; + args.forEach((spec) => { + if (spec !== undefined && spec !== null) { + if (typeof (spec) === 'string' && spec !== '') buffer.push(spec); + else if (typeof (spec) === 'object') { + const cs = Object.keys(spec); + cs.forEach((c) => { if (spec[c]) buffer.push(c); }); + } + } + }); + return buffer.join(' '); +} + +// --- please the linter + +export default {}; + +// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/controls/buttons.tsx b/ivette/src/dome/src/renderer/controls/buttons.tsx index 0789962f9c941cebc455ec5c53ea11d0b2cce94d..f3f2c0a38ce7e4039e988a00d6aa2f87d0c41d12 100644 --- a/ivette/src/dome/src/renderer/controls/buttons.tsx +++ b/ivette/src/dome/src/renderer/controls/buttons.tsx @@ -8,18 +8,21 @@ */ import React from 'react'; +import { classes } from 'dome/misc/utils'; import { Icon } from './icons'; import { LabelProps } from './labels'; import './style.css'; -const DISABLED = ({ disabled = false, enabled = true }) => !!disabled || !enabled; - interface EVENT { stopPropagation: () => void; } +const DISABLED = ({ disabled = false, enabled = true }) => ( + !!disabled || !enabled +); + const TRIGGER = (onClick?: () => void) => (evt?: EVENT) => { - evt && evt.stopPropagation(); + evt?.stopPropagation(); if (onClick) onClick(); }; @@ -27,13 +30,15 @@ const TRIGGER = (onClick?: () => void) => (evt?: EVENT) => { // --- LCD // -------------------------------------------------------------------------- -const LCDCLASS = 'dome-xButton dome-xBoxButton dome-text-code dome-xButton-lcd '; - /** Button-like label. */ export function LCD(props: LabelProps) { + const className = classes( + 'dome-xButton dome-xBoxButton dome-text-code dome-xButton-lcd ', + props.className, + ); return ( <label - className={LCDCLASS + (props.className || '')} + className={className} title={props.title} style={props.style} > @@ -42,7 +47,7 @@ export function LCD(props: LabelProps) { {props.children} </label> ); -}; +} // -------------------------------------------------------------------------- // --- Led @@ -72,11 +77,15 @@ export interface LEDprops { } export const LED = (props: LEDprops) => { - const classes = 'dome-xButton-led dome-xButton-led-' - + (props.status || 'inactive') - + (props.blink ? ' dome-xButton-blink' : '') - + (props.className ? ' ' + props.className : ''); - return (<div className={classes} title={props.title} style={props.style} />); + const className = classes( + 'dome-xButton-led', + `dome-xButton-led-${props.status || 'inactive'}`, + props.blink && 'dome-xButton-blink', + props.className, + ); + return ( + <div className={className} title={props.title} style={props.style} /> + ); }; // -------------------------------------------------------------------------- @@ -89,19 +98,26 @@ const HIDDEN: React.CSSProperties = { visibility: 'hidden' }; interface LABELprops { disabled: boolean; label: string; -}; +} const LABEL = ({ disabled, label }: LABELprops) => ( - <div className="dome-xButton-label" > - <div className="dome-xButton-label dome-control-enabled" - style={disabled ? HIDDEN : VISIBLE} >{label}</div> - <div className="dome-xButton-label dome-control-disabled" - style={disabled ? VISIBLE : HIDDEN}>{label}</div> + <div className="dome-xButton-label"> + <div + className="dome-xButton-label dome-control-enabled" + style={disabled ? HIDDEN : VISIBLE} + >{label} + </div> + <div + className="dome-xButton-label dome-control-disabled" + style={disabled ? VISIBLE : HIDDEN} + >{label} + </div> </div> ); export type ButtonKind = - undefined | 'default' | 'active' | 'primary' | 'warning' | 'positive' | 'negative'; + undefined | 'default' | + 'active' | 'primary' | 'warning' | 'positive' | 'negative'; export interface ButtonProps { /** Text of the label. Prepend to other children elements. */ @@ -151,18 +167,23 @@ export interface ButtonProps { /** Standard button. */ export function Button(props: ButtonProps) { const disabled = props.onClick ? DISABLED(props) : true; - const { focusable = false, kind = 'default', + const { + focusable = false, kind = 'default', visible = true, display = true, blink = false, - selected, icon, label, className = '' } = props; - const theClass = 'dome-xButton dome-xBoxButton dome-xButton-' - + (selected ? 'selected' : kind) - + (!blink ? '' : ' dome-xButton-blink') - + (visible ? '' : ' dome-control-hidden') - + (display ? '' : ' dome-control-erased') - + (className ? ' ' + className : ''); + selected, icon, label, className = '', + } = props; + const theClass = classes( + 'dome-xButton dome-xBoxButton', + `dome-xButton-${selected ? 'selected' : kind}`, + blink && 'dome-xButton-blink', + !visible && 'dome-control-hidden', + !display && 'dome-control-erased', + className, + ); const nofocus = focusable ? undefined : true; return ( - <button type='button' + <button + type="button" className={theClass} disabled={disabled} onClick={TRIGGER(props.onClick)} @@ -175,7 +196,7 @@ export function Button(props: ButtonProps) { {label && <LABEL disabled={disabled} label={label} />} </button> ); -}; +} // -------------------------------------------------------------------------- // --- Icon Button @@ -184,18 +205,23 @@ export function Button(props: ButtonProps) { /** Circled Icon Button. The label property is ignored. */ export const CircButton = (props: ButtonProps) => { const disabled = props.onClick ? DISABLED(props) : true; - const { focusable = false, kind = 'default', + const { + focusable = false, kind = 'default', visible = true, display = true, - selected, icon, blink, className = '' } = props; - const theClass = 'dome-xButton dome-xCircButton dome-xButton-' - + (selected ? 'selected' : kind) - + (!blink ? '' : ' dome-xButton-blink') - + (visible ? '' : ' dome-control-hidden') - + (display ? '' : ' dome-control-erased') - + (className ? ' ' + className : ''); + selected, icon, blink, className = '', + } = props; + const theClass = classes( + 'dome-xButton dome-xCircButton', + `dome-xButton-${selected ? 'selected' : kind}`, + blink && 'dome-xButton-blink', + !visible && 'dome-control-hidden', + !display && 'dome-control-erased', + className, + ); const nofocus = focusable ? undefined : true; return ( - <button type='button' + <button + type="button" className={theClass} disabled={disabled} onClick={TRIGGER(props.onClick)} @@ -259,15 +285,17 @@ export function IconButton(props: IconButtonProps) { const { icon, title, className, visible = true, display = true, selected, - kind = 'default' + kind = 'default', } = props; if (!icon) return null; - const theClass = 'dome-xIconButton' - + ' dome-xIconButton-' + (selected ? 'selected' : kind) - + (disabled ? ' dome-control-disabled' : ' dome-control-enabled') - + (visible ? '' : ' dome-control-hidden') - + (display ? '' : ' dome-control-erased') - + (className ? ' ' + className : ''); + const theClass = classes( + 'dome-xIconButton', + `dome-xIconButton-${selected ? 'selected' : kind}`, + (disabled ? 'dome-control-disabled' : 'dome-control-enabled'), + !visible && 'dome-control-hidden', + !display && 'dome-control-erased', + className, + ); return ( <Icon id={icon} @@ -279,7 +307,7 @@ export function IconButton(props: IconButtonProps) { onClick={TRIGGER(disabled ? undefined : props.onClick)} /> ); -}; +} // -------------------------------------------------------------------------- // --- CheckBox @@ -318,11 +346,14 @@ export const Checkbox = (props: CheckProps) => { <label title={props.title} style={props.style} - className={baseClass + labelClass} > - <input type="checkbox" + className={baseClass + labelClass} + > + <input + type="checkbox" disabled={disabled} checked={value} - onChange={callback} /> + onChange={callback} + /> {props.label} </label> ); @@ -334,9 +365,11 @@ export const Switch = (props: CheckProps) => { const disabled = onChange ? DISABLED(props) : true; const iconId = props.value ? 'SWITCH.ON' : 'SWITCH.OFF'; const onClick = onChange && (() => onChange(!value)); - const className = 'dome-xSwitch ' - + (disabled ? 'dome-control-disabled' : 'dome-control-enabled') - + (props.className ? ' ' + props.className : ''); + const className = classes( + 'dome-xSwitch', + (disabled ? 'dome-control-disabled' : 'dome-control-enabled'), + props.className, + ); return ( <label title={props.title} @@ -387,13 +420,18 @@ export function Radio<A>(props: RadioProps<A>) { <label title={props.title} style={props.style} - className={baseClass + labelClass} > - <input type="radio" - disabled={disabled} checked={checked} onChange={onChange} /> + className={baseClass + labelClass} + > + <input + type="radio" + disabled={disabled} + checked={checked} + onChange={onChange} + /> {props.label} </label> ); -}; +} // -------------------------------------------------------------------------- // --- Radio Group @@ -410,15 +448,15 @@ export interface RadioGroupProps<A> { onChange?: (newValue: A) => void; /** Default selected value. */ className?: string; - /** Additional style for the `<dov/>` container of Raiods */ + /** Additional style for the `< dov /> ` container of Raiods */ style?: React.CSSProperties; /** [[Radio]] Buttons. */ children: any; -}; +} /** - Selector of Radio Buttons. - Childrens of the `RadioGroup` shall be [[Radio]] buttons. + Selector of Radio Buttons. Childrens of the `RadioGroup` shall be [[Radio]] + buttons. The selected value of the group is broadcasted to the radio buttons. Their callbacks are activated _before_ the radio group one, if any. @@ -428,26 +466,34 @@ export interface RadioGroupProps<A> { group is enabled, the `disabled` property of each radio button is taken into account. - The radio buttons inside a group are laidout in a vertical box with the additional - styling properties. - */ + The radio buttons inside a group are laidout in a vertical box with the + additional styling properties. +*/ export function RadioGroup<A>(props: RadioGroupProps<A>) { - const { className = '', style, value: selection, onChange: onGroupSelect } = props; + const { + className = '', + style, + value: selection, + onChange: onGroupSelect, + } = props; const disabledGroup = onGroupSelect ? DISABLED(props) : true; const makeRadio = (elt: any) => { const radioProps = elt.props as RadioProps<A>; const disabled = disabledGroup || DISABLED(radioProps); const { onSelection: onRadioSelect } = radioProps; const onSelection = (v: A) => { - onRadioSelect && onRadioSelect(v); - onGroupSelect && onGroupSelect(v); + if (onRadioSelect) onRadioSelect(v); + if (onGroupSelect) onGroupSelect(v); }; return React.cloneElement(elt, { - disabled, enabled: !disabled, selection, onSelection + disabled, + enabled: !disabled, + selection, + onSelection, }); }; return ( - <div className={'dome-xRadio-group ' + className} style={style}> + <div className={`dome - xRadio - group ${className} `} style={style}> {React.Children.map(props.children, makeRadio)} </div> ); @@ -474,7 +520,7 @@ export interface SelectProps { onChange?: (newValue?: string) => void; /** Default selected value. */ className?: string; - /** Additional style for the `<dov/>` container of Raiods */ + /** Additional style for the `< dov /> ` container of Raiods */ style?: React.CSSProperties; /** Shall be [[Item]] elements. */ children: any; @@ -483,34 +529,37 @@ export interface SelectProps { /** Menu Button. - The different options shall be specified with HTML `<option/>` and `<optgroup />` elements. + The different options shall be specified with HTML + `< option/>` and `<optgroup/>` elements. Options and group shall be specified as follows: - <optgroup label='…'>…</optgroup> - <option value='…' disabled=… >…</option> + * <optgroup label='…'>…</optgroup> + * <option value='…' disabled=… >…</option> - **Warning:** most non-positionning CSS properties might not work on the`<select>` element due - to the native rendering used by Chrome. - You might use`-webkit-appearance: none` to cancel this behavior, you will have to restyle the + **Warning:** most non-positionning CSS properties might not + work on the`<select>` element due to the native rendering used + by Chrome. + You might use `-webkit-appearance: none` to cancel this behavior, + you will have to restyle the component entirely, which is quite ugly by default. */ export function Select(props: SelectProps) { const { onChange, className = '', placeholder } = props; const disabled = onChange ? DISABLED(props) : true; const callback = (evt: React.ChangeEvent<HTMLSelectElement>) => { - onChange && onChange(evt.target.value); + if (onChange) onChange(evt.target.value); }; return ( <select id={props.id} disabled={disabled} - className={'dome-xSelect ' + className} + className={`dome - xSelect ${className} `} style={props.style} title={props.title} value={props.value} onChange={callback} > - {placeholder && <option value=''>— {placeholder} —</option>} + {placeholder && <option value="">— {placeholder} —</option>} {props.children} </select> ); @@ -533,7 +582,7 @@ export interface FieldProps { disabled?: boolean; /** Default fo `false`. */ autoFocus?: boolean; - /** Currently selected value (updated on `ENTER` key)*/ + /** Currently selected value (updated on `ENTER` key) */ value?: string; /** Callback on `ENTER` key. */ onChange?: (newValue: string) => void; @@ -541,7 +590,7 @@ export interface FieldProps { onEdited?: (tmpValue: string) => void; /** Default selected value. */ className?: string; - /** Additional style for the `<dov/>` container of Raiods */ + /** Additional style for the `< dov /> ` container of Raiods */ style?: React.CSSProperties; } @@ -554,31 +603,36 @@ export const Field = (props: FieldProps) => { const disabled = onChange ? DISABLED(props) : true; const theValue = current ?? value; const ONCHANGE = (evt: React.ChangeEvent<HTMLInputElement>) => { - let text = evt.target.value || ''; + const text = evt.target.value || ''; setCurrent(text); - onEdited && onEdited(text); + if (onEdited) onEdited(text); }; const ONKEYPRESS = (evt: React.KeyboardEvent) => { switch (evt.key) { case 'Enter': setCurrent(undefined); - onChange && current && onChange(current); + if (onChange && current) onChange(current); break; case 'Escape': setCurrent(undefined); break; - }; + default: + break; + } }; return ( - <input id={props.id} type='text' + <input + id={props.id} + type="text" autoFocus={!disabled && props.autoFocus} value={theValue} - className={'dome-xField ' + className} + className={`dome - xField ${className} `} style={props.style} disabled={disabled} placeholder={props.placeholder} onKeyPress={ONKEYPRESS} - onChange={ONCHANGE} /> + onChange={ONCHANGE} + /> ); }; diff --git a/ivette/src/dome/src/renderer/controls/icons.tsx b/ivette/src/dome/src/renderer/controls/icons.tsx index a15c2642911594d421d221b8355de6def6d2aa82..d6547bf5a8d89165ae31e9d4d1a9154aa4ffb33a 100644 --- a/ivette/src/dome/src/renderer/controls/icons.tsx +++ b/ivette/src/dome/src/renderer/controls/icons.tsx @@ -12,11 +12,12 @@ import _ from 'lodash'; import React from 'react'; +import { classes } from 'dome/misc/utils'; import Gallery from './gallery.json'; import './style.css'; -/*@ internal */ -const Icons: { [id: string]: { viewBox?: string, path: string } } = Gallery; +/* @ internal */ +const Icons: { [id: string]: { viewBox?: string; path: string } } = Gallery; // -------------------------------------------------------------------------- // --- Raw SVG element @@ -48,9 +49,9 @@ export function SVG(props: SVGprops): null | JSX.Element { const { title, size = 12, - offset = _.floor(- size * 0.125), + offset = _.floor(-size * 0.125), } = props; - const { path, viewBox = "0 0 24 24" } = icon; + const { path, viewBox = '0 0 24 24' } = icon; return ( <svg height={size} @@ -77,7 +78,7 @@ export interface IconProps extends SVGprops { /** Fill style property. */ fill?: string; /** Click callback. */ - onClick?: (event?: React.MouseEvent) => void; + onClick?: (event?: React.MouseEvent<HTMLDivElement>) => void; } /** @@ -85,8 +86,11 @@ export interface IconProps extends SVGprops { Consult the [Icon Gallery](../guides/icons.md.html) for default icons. */ export function Icon(props: IconProps) { - const { id, title, onClick, fill, size, className = '', offset, style } = props; - const divClass = 'dome-xIcon ' + className; + const { + id, title, onClick, fill, + size, className = '', offset, style, + } = props; + const divClass = classes('dome-xIcon', className); const divStyle = fill ? { fill, ...style } : style; return ( <div @@ -130,13 +134,16 @@ export function Badge(props: BadgeProps) { } else { const style = (typeof (value) === 'number' && value < 10) || - (typeof (value) === 'string' && value.length == 1) ? + (typeof (value) === 'string' && value.length === 1) ? { paddingLeft: 2, paddingRight: 2 } : {}; - content = <label style={style} className='dome-text-label'>{value}</label>; + content = <label style={style} className="dome-text-label">{value}</label>; } return ( - <div className="dome-xBadge" - title={title} onClick={onClick}> + <div + className="dome-xBadge" + title={title} + onClick={onClick} + > {content} </div> ); @@ -169,10 +176,11 @@ export function register(icon: CustomIcon) { See [[register]] to add custom icons to the gallery. */ export function forEach(fn: (ico: CustomIcon) => void) { - for (let id in Icons) { + const ids = Object.keys(Icons); + ids.forEach((id) => { const jsicon = Icons[id]; fn({ id, ...jsicon }); - } + }); } // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/controls/labels.tsx b/ivette/src/dome/src/renderer/controls/labels.tsx index 43273c5ee1505b8cf6aec5548c6157b452315a17..28c1b23777a292e970418364efb5d4e4c4afd22d 100644 --- a/ivette/src/dome/src/renderer/controls/labels.tsx +++ b/ivette/src/dome/src/renderer/controls/labels.tsx @@ -8,6 +8,7 @@ */ import React from 'react'; +import { classes } from 'dome/misc/utils'; import { Icon } from './icons'; import './style.css'; @@ -34,14 +35,17 @@ export interface LabelProps { const makeLabel = (className: string, props: LabelProps) => { const { display = true } = props; - const allClasses = - className + - (display ? ' ' : ' dome-control-erased ') + - (props.className || ''); + const allClasses = classes( + className, + !display && 'dome-control-erased', + props.className, + ); return ( - <label className={allClasses} + <label + className={allClasses} title={props.title} - style={props.style} > + style={props.style} + > {props.icon && <Icon title={props.title} id={props.icon} />} {props.label} {props.children} @@ -53,11 +57,11 @@ const makeLabel = (className: string, props: LabelProps) => { // --- CSS Classes // -------------------------------------------------------------------------- -const LABEL = "dome-xLabel dome-text-label"; -const TITLE = "dome-xLabel dome-text-title"; -const DESCR = "dome-xLabel dome-text-descr"; -const TDATA = "dome-xLabel dome-text-data"; -const TCODE = "dome-xLabel dome-text-code"; +const LABEL = 'dome-xLabel dome-text-label'; +const TITLE = 'dome-xLabel dome-text-title'; +const DESCR = 'dome-xLabel dome-text-descr'; +const TDATA = 'dome-xLabel dome-text-data'; +const TCODE = 'dome-xLabel dome-text-code'; // -------------------------------------------------------------------------- // --- Components