Skip to content

Commit

Permalink
feat: add radio component (#2985)
Browse files Browse the repository at this point in the history
* adding radio web component
  • Loading branch information
marjonlynch authored Apr 24, 2020
1 parent b69d5bb commit fffae9f
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 6 deletions.
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 {
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}"
?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

0 comments on commit fffae9f

Please sign in to comment.