Skip to content
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

Rewrite Button to Typescript #2984

Merged
merged 12 commits into from
Aug 22, 2021
75 changes: 0 additions & 75 deletions js/src/common/components/Button.js

This file was deleted.

130 changes: 130 additions & 0 deletions js/src/common/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type Mithril from 'mithril';
import Component, { ComponentAttrs } from '../Component';
import fireDebugWarning from '../helpers/fireDebugWarning';
import icon from '../helpers/icon';
import classList from '../utils/classList';
import extractText from '../utils/extractText';
import LoadingIndicator from './LoadingIndicator';

export interface IButtonAttrs extends ComponentAttrs {
/**
* Class(es) of an optional icon to be rendered within the button.
*
* If provided, the button will gain a `has-icon` class.
*/
icon?: string;
/**
* Disables button from user input.
*
* Default: `false`
*/
disabled?: boolean;
/**
* Show a loading spinner within the button.
*
* If `true`, also disables the button.
*
* Default: `false`
*/
loading?: boolean;
/**
* **DEPRECATED:** Please use the `aria-label` attribute instead. For tooltips, use
* the `<Tooltip>` component.
*
* Accessible text for the button. This should always be present if the button only
* contains an icon.
*
* The textual content of this attribute is passed to the DOM element as `aria-label`.
*
* @deprecated
*/
title?: string | Mithril.ChildArray;
/**
* Accessible text for the button. This should always be present if the button only
* contains an icon.
*
* The textual content of this attribute is passed to the DOM element as `aria-label`.
*/
'aria-label'?: string | Mithril.ChildArray;
/**
* Button type.
*
* Default: `"button"`
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type
*/
type?: string;
}

/**
* The `Button` component defines an element which, when clicked, performs an
* action.
*
* Other attrs will be assigned as attributes on the `<button>` element.
*
* Note that a Button has no default class names. This is because a Button can
* be used to represent any generic clickable control, like a menu item. Common
* styles can be applied by providing `className="Button"` to the Button component.
*/
export default class Button extends Component<IButtonAttrs> {
davwheat marked this conversation as resolved.
Show resolved Hide resolved
view(vnode: Mithril.Vnode<IButtonAttrs, never>) {
let { type, title, 'aria-label': ariaLabel, icon: iconName, disabled, loading, className, class: _class, ...attrs } = this.attrs;

// If no `type` attr provided, set to "button"
type ||= 'button';

// Use `title` attribute as `aria-label` if none provided
ariaLabel ||= title;

// If given a translation object, extract the text.
if (typeof ariaLabel === 'object') {
ariaLabel = extractText(ariaLabel);
}

if (disabled || loading) {
delete attrs.onclick;
}

className = classList(_class, className, {
hasIcon: iconName,
disabled: disabled || loading,
loading: loading,
});

const buttonAttrs = {
disabled,
className,
type,
'aria-label': ariaLabel,
...attrs,
};

return <button {...buttonAttrs}>{this.getButtonContent(vnode.children)}</button>;
}

oncreate(vnode: Mithril.VnodeDOM<IButtonAttrs, this>) {
super.oncreate(vnode);

const { 'aria-label': ariaLabel } = this.attrs;

if (!ariaLabel && !extractText(vnode.children) && !this.element?.getAttribute?.('aria-label')) {
davwheat marked this conversation as resolved.
Show resolved Hide resolved
fireDebugWarning(
'[Flarum Accessibility Warning] Button has no content and no accessible label. This means that screen-readers will not be able to interpret its meaning and just read "Button". Consider providing accessible text via the `aria-label` attribute. https://web.dev/button-name',
this.element
);
}
}

/**
* Get the template for the button's content.
*/
protected getButtonContent(children: Mithril.Children): Mithril.ChildArray {
const iconName = this.attrs.icon;

return [
iconName && icon(iconName, { className: 'Button-icon' }),
children && <span className="Button-label">{children}</span>,
this.attrs.loading && <LoadingIndicator size="small" display="inline" />,
];
}
}
13 changes: 8 additions & 5 deletions js/src/common/components/TextEditorButton.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import extractText from '../utils/extractText';
import Button from './Button';
import Tooltip from './Tooltip';

/**
* The `TextEditorButton` component displays a button suitable for the text
* editor toolbar.
*
* Automatically creates tooltips using the Tooltip component and provided text.
*
* ## Attrs
* - `title` - Tooltip for the button
*/
export default class TextEditorButton extends Button {
view(vnode) {
const originalView = super.view(vnode);

// Steal tooltip label from the Button superclass
const tooltipText = originalView.attrs.title;
delete originalView.attrs.title;

return <Tooltip text={tooltipText}>{originalView}</Tooltip>;
return <Tooltip text={this.attrs.tooltipText || extractText(vnode.children)}>{originalView}</Tooltip>;
}

static initAttrs(attrs) {
super.initAttrs(attrs);

attrs.className = attrs.className || 'Button Button--icon Button--link';
attrs.tooltipText = attrs.title;
}
}
16 changes: 16 additions & 0 deletions js/src/common/helpers/fireDebugWarning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Calls `console.warn` with the provided arguments, but only if the forum is in debug mode.
*
* This function is intended to provide warnings to extension developers about issues with
* their extensions that may not be easily noticed when testing, such as accessibility
* issues.
*
* These warnings should be hidden on production forums to ensure webmasters are not
* inundated with do-gooders telling them they have an issue when it isn't something they
* can fix.
*/
export default function fireDebugWarning(...args: any[]): void {
davwheat marked this conversation as resolved.
Show resolved Hide resolved
if (!app.forum.attribute('debug')) return;

console.warn(...args);
}