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

feat: add radio component #2985

Merged
merged 12 commits into from
Apr 24, 2020
4 changes: 4 additions & 0 deletions packages/web-components/fast-components/src/radio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# fast-radio
An implementation of a [radio](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio) as a form-connected web-component.

For more information view the [component specification](./radio.spec.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<fast-design-system-provider use-defaults>
<h1>Radio</h1>

<h4>Defaults</h4>
<fast-radio></fast-radio>
<div>
<fast-radio>label</fast-radio>
</div>

<h4>Checked</h4>
<fast-radio value="checked" checked></fast-radio>

<!-- Required -->
<h4>Required</h4>
<fast-radio value="required" required></fast-radio>

<!-- Disabled -->
<h4>Disabled</h1>
<fast-radio disabled></fast-radio>
<fast-radio disabled>label</fast-radio>
<fast-radio disabled checked>checked</fast-radio>

<h4>Visual vs audio label</h4>
<fast-radio>
<span aria-label="Audio label">Visible label</span>
</fast-radio>

<div style="display: flex; flex-direction: column;margin-top: 12px;">
<label id="label1">Outside label</label>
<fast-radio aria-labelledby="label1"></fast-radio>
</div>
</fast-design-system-provider>
14 changes: 14 additions & 0 deletions packages/web-components/fast-components/src/radio/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { customElement } from "@microsoft/fast-element";
import { Radio } from "./radio";
import { RadioTemplate as template } from "./radio.template";
import { RadioStyles as styles } from "./radio.styles";

@customElement({
name: "fast-radio",
template,
styles,
})
export class FASTRadio extends Radio {}
export * from "./radio.template";
export * from "./radio.styles";
export * from "./radio";
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ Extends [form associated custom element](../form-associated/form-associated-cust
- `disabled`
- The radio should be disabled from user interaction and will not be submitted with the form data.
- `value` - Not visible to the user, it's used for form data and to distinguish between other radio buttons of the same name attribute value.
- `name` - All radio buttons with the same name will operate as a group allowing only
one radio within the group to be selected at a time.
- `checked`
- The initial checked value.

Expand Down Expand Up @@ -87,9 +85,6 @@ one radio within the group to be selected at a time.
The checked state can be toggled by:
- Clicking the radio button (or any of it's labels)
- Pressing the space-bar while focus is placed on the radio button will toggle it on
- Pressing the right/top or left/bottom arrow keys will move the focus and toggle on the checked value for the radio button receiving focus.
- Adding / removing the "checked" content attribute
- This will only trigger a change if the "checked" property has not been changed, either through user action or programmatically

**disabled**: `true` or `false`
When disabled, the value will not be changeable through user interaction. It should also not expose it's value to a form submission.
Expand Down
13 changes: 13 additions & 0 deletions packages/web-components/fast-components/src/radio/radio.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FASTDesignSystemProvider } from "../design-system-provider";
import Examples from "./fixtures/base.html";
import { FASTRadio } from ".";

// Prevent tree-shaking
FASTRadio;
FASTDesignSystemProvider;

export default {
title: "Radio",
};

export const Radio = () => Examples;
137 changes: 137 additions & 0 deletions packages/web-components/fast-components/src/radio/radio.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { css } from "@microsoft/fast-element";
import { disabledCursor, display } from "../styles";
import { focusVisible } from "../styles/focus";
import { SystemColors } from "../styles/system-colors";
import { heightNumber } from "../styles/size";
import {
neutralFillInputHoverBehavior,
neutralFillInputRestBehavior,
neutralForegroundRestBehavior,
neutralOutlineHoverBehavior,
neutralOutlineRestBehavior,
} from "../styles/recipes";

export const RadioStyles = css`
${display("inline-flex")} :host {
--input-size: calc((${heightNumber} / 2) + var(--design-unit));
align-items: center;
outline: none;
margin: calc(var(--design-unit) * 1px) 0;
${
/*
* Chromium likes to select label text or the default slot when
* the radio button is clicked. Maybe there is a better solution here?
*/ ""
} user-select: none;
position: relative;
flex-direction: row;
transition: all 0.2s ease-in-out;
}

.control {
position: relative;
width: calc(var(--input-size) * 1px);
height: calc(var(--input-size) * 1px);
box-sizing: border-box;
border-radius: 50%;
border: calc(var(--outline-width) * 1px) solid var(--neutral-outline-rest);
background: var(--neutral-fill-input-rest);
outline: none;
cursor: pointer;
}

.label {
font-family: var(--body-font);
color: var(--neutral-foreground-rest);
${
/* Need to discuss with Brian how HorizontalSpacingNumber can work. https://github.com/microsoft/fast-dna/issues/2766 */ ""
} padding-inline-start: calc(var(--design-unit) * 2px + 2px);
margin-inline-end: calc(var(--design-unit) * 2px + 2px);
cursor: pointer;
${
/* Font size is temporary - replace when adaptive typography is figured out */ ""
} font-size: calc(1rem + (var(--density) * 2px));
}

.checked-indicator {
marjonlynch marked this conversation as resolved.
Show resolved Hide resolved
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
background: var(--neutral-foreground-rest);
fill: var(--neutral-foreground-rest);
opacity: 0;
pointer-events: none;
}

.control:hover {
background: var(--neutral-fill-input-hover);
border-color: var(--neutral-outline-hover);
}

:host(:${focusVisible}) .control {
box-shadow: 0 0 0 1px var(--neutral-focus) inset;
border-color: var(--neutral-focus);
}

:host(.disabled) .label,
:host(.readonly) .label,
:host(.readonly) .control,
:host(.disabled) .control {
cursor: ${disabledCursor};
}

:host(.checked) .checked-indicator {
opacity: 1;
}

:host(.disabled) {
opacity: var(--disabled-opacity);
}

@media (forced-colors: active) {
.control, .control:hover, .control:active {
forced-color-adjust: none;
border-color: ${SystemColors.FieldText};
background: ${SystemColors.Field};
}

.checked-indicator {
fill: ${SystemColors.FieldText};
}

:host(:${focusVisible}) .control {
border-color: ${SystemColors.Highlight};
}

:host(.disabled) {
opacity: 1;
}

:host(.disabled) .label {
forced-color-adjust: none;
color: ${SystemColors.GrayText};
}

:host(.disabled) .control {
forced-color-adjust: none;
border-color: ${SystemColors.GrayText};
}

:host(.disabled) .checked-indicator {
forced-color-adjust: none;
fill: ${SystemColors.GrayText};
background: ${SystemColors.GrayText};
}
}
`.withBehaviors(
neutralFillInputHoverBehavior,
neutralFillInputRestBehavior,
neutralForegroundRestBehavior,
neutralOutlineHoverBehavior,
neutralOutlineRestBehavior
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { html, when } from "@microsoft/fast-element";
import { Radio } from "./radio";

export const RadioTemplate = html<Radio>`
<template
role="radio"
class="${x => (x.checked ? "checked" : "")} ${x =>
x.readOnly ? "readonly" : ""}"
aria-checked="${x => x.checked}"
marjonlynch marked this conversation as resolved.
Show resolved Hide resolved
?aria-required="${x => x.required}"
?aria-disabled="${x => x.disabled}"
?aria-readonly="${x => x.readOnly}"
tabindex="${x => (x.disabled ? null : 0)}"
@keypress="${(x, c) => x.keypressHandler(c.event as KeyboardEvent)}"
@click="${(x, c) => x.clickHandler(c.event as MouseEvent)}"
>
<div part="control" class="control">
<slot name="checked-indicator">
<div part="checked-indicator" class="checked-indicator"></div>
</slot>
</div>
${when(
x => x.childNodes.length,
html`
<label part="label" class="label">
<slot></slot>
</label>
`
)}
</template>
`;
119 changes: 119 additions & 0 deletions packages/web-components/fast-components/src/radio/radio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { attr, observable } from "@microsoft/fast-element";
import { keyCodeSpace } from "@microsoft/fast-web-utilities";
import { FormAssociated } from "../form-associated";

export class Radio extends FormAssociated<HTMLInputElement> {
@attr({ attribute: "readonly", mode: "boolean" })
public readOnly: boolean; // Map to proxy element
private readOnlyChanged(): void {
if (this.proxy instanceof HTMLElement) {
this.proxy.readOnly = this.readOnly;
}
}

@attr
public name: string; // Map to proxy element
protected nameChanged(): void {
if (this.proxy instanceof HTMLElement) {
this.proxy.name = this.name;
}
}

/**
* The element's value to be included in form submission when checked.
* Default to "on" to reach parity with input[type="radio"]
*/
public value: string = "on"; // Map to proxy element.
private valueChanged(): void {
if (this.proxy instanceof HTMLElement) {
this.proxy.value = this.value;
}
}

/**
* Provides the default checkedness of the input element
* Passed down to proxy
*/
@attr({ attribute: "checked", mode: "boolean" })
public checkedAttribute: boolean;
private checkedAttributeChanged(): void {
this.defaultChecked = this.checkedAttribute;
}

/**
* Initialized to the value of the checked attribute. Can be changed independently of the "checked" attribute,
* but changing the "checked" attribute always additionally sets this value.
*/
@observable
public defaultChecked: boolean = !!this.checkedAttribute;
private defaultCheckedChanged(): void {
if (!this.dirtyChecked) {
// Setting this.checked will cause us to enter a dirty state,
// but if we are clean when defaultChecked is changed, we want to stay
// in a clean state, so reset this.dirtyChecked
this.checked = this.defaultChecked;
this.dirtyChecked = false;
}
}

/**
* The checked state of the control
*/
@observable
public checked: boolean = this.defaultChecked;
private checkedChanged(): void {
if (!this.dirtyChecked) {
this.dirtyChecked = true;
}

if (this.proxy instanceof HTMLElement) {
this.proxy.checked = this.checked;
}

this.$emit("change");
this.checkedAttribute = this.checked;
this.updateForm();
}

protected proxy: HTMLInputElement = document.createElement("input");

/**
* Tracks whether the "checked" property has been changed.
* This is necessary to provide consistent behavior with
* normal input radios
*/
private dirtyChecked: boolean = false;

constructor() {
super();
this.proxy.setAttribute("type", "radio");
}

public connectedCallback(): void {
super.connectedCallback();
this.updateForm();
}

private updateForm(): void {
const value = this.checked ? this.value : null;
this.setFormValue(value, value);
}

public keypressHandler = (e: KeyboardEvent): void => {
super.keypressHandler(e);
switch (e.keyCode) {
case keyCodeSpace:
if (!this.checked) {
this.checked = true;
}
break;
}
};

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
public clickHandler = (e: MouseEvent): void => {
if (!this.disabled && !this.readOnly) {
this.checked = !this.checked;
}
};
}
3 changes: 2 additions & 1 deletion specs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ Here you'll find specifications for custom elements and other library features.
| [Menu](./menu/menu.md) | :white_check_mark: | -- |
| [Number field](./number-field.md) | :white_check_mark: | -- |
| [Progress](../packages/web-components/fast-components/src/progress/progress.spec.md) | :white_check_mark: | :white_check_mark: |
| [Radio](../packages/web-components/fast-components/src/radio/radio.spec.md) | :white_check_mark: | -- |
| [Radio](../packages/web-components/fast-components/src/radio/radio.spec.md) | :white_check_mark: | :white_check_mark: |
RadioGroup | -- | -- |
| Rating | -- | -- |
| Select | -- | -- |
| [Slider](../packages/web-components/fast-components/src/slider/slider.spec.md) | :white_check_mark: | :white_check_mark: |
Expand Down