Skip to content

Commit

Permalink
feat: add checkbox, radio, toggle components (#175)
Browse files Browse the repository at this point in the history
* feat: add checkbox

* refacto

* feat: add radio

* Update README.md

* feat: add toggle

* fix: merge

* docs

* feat: avoid input to be shrink

* feat: add label property, add inputId for radio
  • Loading branch information
Kout95 authored Jun 21, 2024
1 parent 263834a commit 608e68a
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 66 deletions.
7 changes: 7 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ This enables supporting multiple searches in the same page
* searchalicious-checkbox is a simple checkbox
* it can be used to replace the default checkbox
* it allows to keep the state of the checkbox when you change the property
* searchalicious-radio is a simple radio button
* it can be used to replace the default radio button
* it allows to keep the state of the radio button when you change the property
* You can unchecked the radio button by clicking on it
* searchalicious-toggle is a simple toggle button
* it can be used to replace the checkbox
* it allows to keep the state of the toggle button when you change the property
* searchalicious-secondary-button is a button with defined style
* it can be used to replace the default button
* searchalicious-button-transparent is a transparent button with defined style
Expand Down
4 changes: 4 additions & 0 deletions frontend/public/icons/checkbox-check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions frontend/src/mixins/checked-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {Constructor} from './utils';
import {LitElement, PropertyValues} from 'lit';
import {property} from 'lit/decorators.js';
import {BasicEvents} from '../utils/enums';

export interface CheckedInputMixinInterface {
checked: boolean;
name: string;
label: string;
getInputElement(): HTMLInputElement | null;
_dispatchChangeEvent(checked: boolean, name: string): void;
refreshInput(): void;
_handleChange(e: {target: HTMLInputElement}): void;
}

export const CheckedInputMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class CheckedInputMixinClass extends superClass {
/**
* Represents the checked state of the input.
* @type {boolean}
*/
@property({type: Boolean})
checked = false;

/**
* Represents the name of the input.
* @type {string}
*/
@property({type: String})
name = '';

/**
* Represents the label of the input.
* @type {string}
*/
@property({type: String})
label = '';

getInputElement() {
return this.shadowRoot?.querySelector('input');
}

_dispatchChangeEvent(checked: boolean, name: string) {
const inputEvent = new CustomEvent(BasicEvents.CHANGE, {
detail: {checked, name},
bubbles: true,
composed: true,
});
this.dispatchEvent(inputEvent);
}
refreshInput() {
const inputElement = this.getInputElement();
if (inputElement) {
inputElement.checked = this.checked;
}

/**
* Called when the element’s DOM has been updated and rendered.
* @param {PropertyValues} _changedProperties - The changed properties.
*/
}
protected override updated(_changedProperties: PropertyValues) {
this.refreshInput();
super.updated(_changedProperties);
}

/**
* Handles the change event on the radio.
* @param {Event} e - The change event.
*/
_handleChange(e: {target: HTMLInputElement}) {
this.checked = e.target.checked;
this._dispatchChangeEvent(this.checked, this.name);
}
}
return CheckedInputMixinClass as Constructor<CheckedInputMixinInterface> & T;
};
2 changes: 2 additions & 0 deletions frontend/src/search-a-licious.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export {SearchaliciousCheckbox} from './search-checkbox';
export {SearchaliciousRadio} from './search-radio';
export {SearchaliciousToggle} from './search-toggle';
export {SearchaliciousBar} from './search-bar';
export {SearchaliciousButton} from './search-button';
export {SearchaliciousPages} from './search-pages';
Expand Down
116 changes: 60 additions & 56 deletions frontend/src/search-checkbox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {LitElement, html, PropertyValues} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
import {CheckedInputMixin} from './mixins/checked-input';

/**
* A custom element that represents a checkbox.
Expand All @@ -9,77 +10,80 @@ import {customElement, property} from 'lit/decorators.js';
* @extends {LitElement}
*/
@customElement('searchalicious-checkbox')
export class SearchaliciousCheckbox extends LitElement {
export class SearchaliciousCheckbox extends CheckedInputMixin(LitElement) {
/**
* Represents the checked state of the checkbox.
* @type {boolean}
* The styles for the checkbox.
* "appearance: none" is used to remove the default checkbox style.
* margin-right: 0 is used to remove the default margin between the checkbox and the label.
* We use an svg icon for the checked state, it is located in the public/icons folder.
* @type {import('lit').CSSResult}
*/
@property({type: Boolean})
checked = false;

/**
* Represents the name of the checkbox.
* @type {string}
*/
@property({type: String})
name = '';
static override styles = css`
.checkbox-wrapper {
display: flex;
align-items: center;
flex-wrap: wrap;
}
/**
* Refreshes the checkbox to reflect the current state of the `checked` property.
*/
refreshCheckbox() {
const inputElement = this.shadowRoot?.querySelector('input');
if (inputElement) {
inputElement.checked = this.checked;
input[type='checkbox'] {
cursor: pointer;
position: relative;
flex-shrink: 0;
width: 20px;
height: 20px;
margin-right: 0;
appearance: none;
border: 1px solid var(--searchalicious-checkbox-color, black);
background-color: transparent;
}
input[type='checkbox']:checked {
background-color: var(--searchalicious-checkbox-color, black);
}
input[type='checkbox']:focus {
outline: 1px solid var(--searchalicious-checkbox-focus-color, black);
}
input[type='checkbox']:checked:after {
position: absolute;
content: '';
height: 12px;
width: 12px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: url('/static/icons/checkbox-check.svg') no-repeat center
center;
}
}
/**
* Called when the element’s DOM has been updated and rendered.
* @param {PropertyValues} _changedProperties - The changed properties.
*/
protected override updated(_changedProperties: PropertyValues) {
this.refreshCheckbox();
super.updated(_changedProperties);
}
label {
cursor: pointer;
padding-left: 8px;
}
`;

/**
* Renders the checkbox.
* @returns {import('lit').TemplateResult<1>} - The HTML template for the checkbox.
*/
override render() {
return html`
<input
part="checkbox"
.name=${this.name}
.id="${this.name}"
type="checkbox"
?checked=${this.checked}
@change=${this._handleChange}
/>
<div class="checkbox-wrapper">
<input
part="checkbox"
.name=${this.name}
.id="${this.name}"
type="checkbox"
?checked=${this.checked}
@change=${this._handleChange}
/>
<label for="${this.name}"
><slot name="label">${this.label}</slot></label
>
</div>
`;
}

/**
* Handles the change event on the checkbox.
* @param {Event} e - The change event.
*/
_handleChange(e: {target: HTMLInputElement}) {
this.checked = e.target.checked;
const inputEvent = new CustomEvent('change', {
detail: {checked: this.checked, name: this.name},
bubbles: true,
composed: true,
});
this.dispatchEvent(inputEvent);
}
}

declare global {
/**
* The HTMLElementTagNameMap interface represents a map of custom element tag names to custom element constructors.
* Here, it's extended to include 'searchalicious-checkbox' as a valid custom element tag name.
*/
interface HTMLElementTagNameMap {
'searchalicious-checkbox': SearchaliciousCheckbox;
}
Expand Down
19 changes: 9 additions & 10 deletions frontend/src/search-facets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,6 @@ export class SearchaliciousTermsFacet extends SearchActionMixin(
fieldset {
margin-top: 1rem;
}
.term-wrapper {
display: block;
}
.button {
margin-left: auto;
margin-right: auto;
Expand Down Expand Up @@ -383,18 +380,20 @@ export class SearchaliciousTermsFacet extends SearchActionMixin(
*/
renderTerm(term: FacetTerm) {
return html`
<div class="term-wrapper" part="term-wrapper">
<div>
<searchalicious-checkbox
.name=${term.key}
.checked=${this.selectedTerms[term.key]}
@change=${this.onCheckboxChange}
></searchalicious-checkbox>
<label for="${term.key}"
>${term.name}
${term.count
? html`<span part="docCount">(${term.count})</span>`
: nothing}</label
>
<!-- "display: contents;" is used to avoid the wrapping of the span in a div cf https://lit.dev/docs/frameworks/react/#using-slots -->
<div slot="label" style="display: contents;">
${term.name}
${term.count
? html`<span part="docCount">(${term.count})</span>`
: nothing}
</div>
</searchalicious-checkbox>
</div>
`;
}
Expand Down
Loading

0 comments on commit 608e68a

Please sign in to comment.