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 checkbox, radio, toggle components #175

Merged
merged 10 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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