Skip to content

Commit

Permalink
Merge pull request #17 from GreenTeaCake/add-disabled-property
Browse files Browse the repository at this point in the history
Add disabled property
  • Loading branch information
GreenTeaCake committed Apr 10, 2024
2 parents adc608b + 8e378ce commit 8e165fb
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 50 deletions.
27 changes: 22 additions & 5 deletions src/components/switch/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,31 @@

## Overview

Switch component documentation.
A switch is an input widget that allows users to choose one of two values: on or off.
Switches are similar to checkboxes and toggle buttons. But switches can only be used for binary
input while checkboxes and toggle buttons allow implementations the option of supporting a third
middle state. Checkboxes can be checked or not checked and can optionally also allow for a
partially checked state. Toggle buttons can be pressed or not pressed and can optionally allow
for a partially pressed state.

Specifications:

- [`switch` role](https://w3c.github.io/aria/#switch)
- [Switch Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/)
- [Switch Example: A switch based on a div element](https://www.w3.org/WAI/ARIA/apg/patterns/switch/examples/switch/)
- [ARIA: switch role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/switch_role)

`gtc-switch` custom element implements the switch specification behaviour. It is a stateless controlled component.
Please use `checked` property to control the component's state and attach event listener for custom `gtcChange`
event to be notified about the state changes.

## Properties

| Property | Attribute | Description | Type | Default |
| -------------------- | --------- | --------------------------------------------------------------------------------------------------------- | --------- | ----------- |
| `checked` | `checked` | The reflective property that allows to control the state of the switch. If `true`, the switch is checked. | `boolean` | `false` |
| `label` _(required)_ | `label` | Text value used for `aria-label` property. | `string` | `undefined` |
| Property | Attribute | Description | Type | Default |
| -------------------- | ---------- | --------------------------------------------------------------------------------------------------------- | --------- | ----------- |
| `checked` | `checked` | The reflective property that allows to control the state of the switch. If `true`, the switch is checked. | `boolean` | `false` |
| `disabled` | `disabled` | If `true`, the switch is disabled. | `boolean` | `false` |
| `label` _(required)_ | `label` | Text value used for `aria-label` property. The minimal length is 1. | `string` | `undefined` |

## Events

Expand Down
35 changes: 31 additions & 4 deletions src/components/switch/switch.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ $switch-width: 40px;
$switch-height: 20px;

$marker-size: 16px;
$marker-color: #ffffff;

$background-color-default: #c7c7c7;
$background-color-checked: #398368;
$background-color-disabled: #e3e3e3;

$marker-color-default: #ffffff;
$marker-color-disabled: #c7c7c7;

$highlight-color-default: #112c9f;

$spacing: calc(($switch-height - $marker-size) / 2);

Expand All @@ -15,28 +23,47 @@ $spacing: calc(($switch-height - $marker-size) / 2);
width: $switch-width;
height: $switch-height;
border-radius: calc($switch-height / 2);
background-color: #c9c7c7;
background-color: $background-color-default;
position: relative;
cursor: pointer;

&:hover {
outline: 2px solid $highlight-color-default;
outline-offset: 2px;
}

.gtc-switch-marker {
display: inline-block;
width: $marker-size;
height: $marker-size;
border-radius: calc($marker-size / 2);
background-color: $marker-color;
background-color: $marker-color-default;

position: absolute;
top: $spacing;
left: $spacing;
}

&__checked {
background-color: #398368;
background-color: $background-color-checked;

.gtc-switch-marker {
left: calc($switch-width - $marker-size - $spacing);
}
}

&__disabled {
pointer-events: none;
cursor: not-allowed;
background-color: $background-color-disabled;

&:hover {
outline: none;
}

.gtc-switch-marker {
background-color: $marker-color-disabled;
}
}
}
}
13 changes: 10 additions & 3 deletions src/components/switch/switch.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ const meta: Meta = {
component: 'gtc-switch',
tags: ['autodocs'],
render: (args) => {
const { checked, label } = args;
return `<gtc-switch checked="${checked}" label="${label}" />`;
const { label, checked, disabled } = args;
return `
<gtc-switch
label="${label}"
checked="${checked}"
disabled="${disabled}"
/>
`;
},
argTypes: DOCS.argTypes,
parameters: {
Expand All @@ -26,7 +32,8 @@ export default meta;
export const Primary: StoryObj = {
name: 'Usage Example',
args: {
checked: false,
label: 'Switch',
checked: false,
disabled: false,
},
};
52 changes: 44 additions & 8 deletions src/components/switch/switch.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
import { Component, Event, type EventEmitter, Host, Prop, h } from '@stencil/core';
import { Component, Event, type EventEmitter, Host, Prop, h, Watch } from '@stencil/core';

const MIN_LABEL_LENGTH = 1;

/**
* Switch component documentation.
* A switch is an input widget that allows users to choose one of two values: on or off.
* Switches are similar to checkboxes and toggle buttons. But switches can only be used for binary
* input while checkboxes and toggle buttons allow implementations the option of supporting a third
* middle state. Checkboxes can be checked or not checked and can optionally also allow for a
* partially checked state. Toggle buttons can be pressed or not pressed and can optionally allow
* for a partially pressed state.
*
* Specifications:
* - [`switch` role](https://w3c.github.io/aria/#switch)
* - [Switch Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/)
* - [Switch Example: A switch based on a div element](https://www.w3.org/WAI/ARIA/apg/patterns/switch/examples/switch/)
* - [ARIA: switch role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/switch_role)
*
* @link https://www.w3.org/WAI/ARIA/apg/patterns/switch/
* @link https://www.w3.org/WAI/ARIA/apg/patterns/switch/examples/switch/
* @link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/switch_role
* `gtc-switch` custom element implements the switch specification behaviour. It is a stateless controlled component.
* Please use `checked` property to control the component's state and attach event listener for custom `gtcChange`
* event to be notified about the state changes.
*/
@Component({
tag: 'gtc-switch',
shadow: true,
styleUrl: 'switch.scss',
})
export class Switch {
/**
* Text value used for `aria-label` property.
*
* The minimal length is 1.
*/
@Prop({ reflect: true }) public label!: string;

/**
* The reflective property that allows to control the state of the switch.
*
Expand All @@ -21,24 +41,28 @@ export class Switch {
@Prop({ reflect: true }) public checked = false;

/**
* Text value used for `aria-label` property.
* If `true`, the switch is disabled.
*/
@Prop() public label!: string;
@Prop({ reflect: true }) public disabled = false;

/**
* Is called when the value has changed.
*/
// event name is prefixed not to be confused/conflict with the native one
@Event() private gtcChange: EventEmitter<boolean>;

render() {
return (
<Host>
<div
aria-checked={this.checked ? 'true' : 'false'}
aria-disabled={this.disabled ? 'true' : 'false'}
aria-label={this.label}
class={{
// order matters
'gtc-switch': true,
'gtc-switch__checked': this.checked,
'gtc-switch__disabled': this.disabled,
}}
onClick={this.onClick}
onKeyDown={this.onKeyDown}
Expand All @@ -51,9 +75,21 @@ export class Switch {
);
}

@Watch('label')
protected validateLabel(newLabelValue: string) {
if (typeof newLabelValue !== 'string' || newLabelValue === '') {
throw new Error('Switch has no label.');
}
if (newLabelValue.length < MIN_LABEL_LENGTH) {
throw new Error('Switch label is too short.');
}
}

private toggleChecked(event: KeyboardEvent | MouseEvent) {
event.preventDefault();
this.gtcChange.emit(!this.checked);
if (!this.disabled) {
this.gtcChange.emit(!this.checked);
}
}

private onClick = (event: MouseEvent) => {
Expand Down
62 changes: 62 additions & 0 deletions src/components/switch/test/switch.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,66 @@ describe('gtc-switch', () => {
expect(element.getAttribute('aria-checked')).toEqual(`false`);
});
});

describe('emits event and triggers callback if clicked', () => {
it('when checked', async () => {
const page = await newE2EPage();

await page.setContent('<gtc-switch checked="true"></gtc-switch>');

const gtcChange = await page.spyOnEvent('gtcChange');

const element = await page.find('gtc-switch >>> div.gtc-switch');
await element.click();
await page.waitForChanges();

expect(gtcChange).toHaveReceivedEventTimes(1);
expect(gtcChange).toHaveReceivedEventDetail(false);
});

it('when unchecked', async () => {
const page = await newE2EPage();

await page.setContent('<gtc-switch checked="false"></gtc-switch>');

const gtcChange = await page.spyOnEvent('gtcChange');

const element = await page.find('gtc-switch >>> div.gtc-switch');
await element.click();
await page.waitForChanges();

expect(gtcChange).toHaveReceivedEventTimes(1);
expect(gtcChange).toHaveReceivedEventDetail(true);
});
});

describe('emits no event if disabled', () => {
it('when checked', async () => {
const page = await newE2EPage();

await page.setContent('<gtc-switch checked="true" disabled="true"></gtc-switch>');

const gtcChange = await page.spyOnEvent('gtcChange');

const element = await page.find('gtc-switch >>> div.gtc-switch');
await element.click();
await page.waitForChanges();

expect(gtcChange).toHaveReceivedEventTimes(0);
});

it('when unchecked', async () => {
const page = await newE2EPage();

await page.setContent('<gtc-switch checked="false" disabled="true"></gtc-switch>');

const gtcChange = await page.spyOnEvent('gtcChange');

const element = await page.find('gtc-switch >>> div.gtc-switch');
await element.click();
await page.waitForChanges();

expect(gtcChange).toHaveReceivedEventTimes(0);
});
});
});
Loading

0 comments on commit 8e165fb

Please sign in to comment.