diff --git a/frontend/README.md b/frontend/README.md index 89623fba..a68ec74b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -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 diff --git a/frontend/public/icons/checkbox-check.svg b/frontend/public/icons/checkbox-check.svg new file mode 100644 index 00000000..43769237 --- /dev/null +++ b/frontend/public/icons/checkbox-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/mixins/checked-input.ts b/frontend/src/mixins/checked-input.ts new file mode 100644 index 00000000..d0f9be8e --- /dev/null +++ b/frontend/src/mixins/checked-input.ts @@ -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 = >( + 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 & T; +}; diff --git a/frontend/src/search-a-licious.ts b/frontend/src/search-a-licious.ts index f03ec31c..fc2f4e53 100644 --- a/frontend/src/search-a-licious.ts +++ b/frontend/src/search-a-licious.ts @@ -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'; diff --git a/frontend/src/search-checkbox.ts b/frontend/src/search-checkbox.ts index db6bf4ad..12e1f828 100644 --- a/frontend/src/search-checkbox.ts +++ b/frontend/src/search-checkbox.ts @@ -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. @@ -9,39 +10,55 @@ 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. @@ -49,37 +66,24 @@ export class SearchaliciousCheckbox extends LitElement { */ override render() { return html` - +
+ + +
`; } - - /** - * 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; } diff --git a/frontend/src/search-facets.ts b/frontend/src/search-facets.ts index 86194594..3353fa86 100644 --- a/frontend/src/search-facets.ts +++ b/frontend/src/search-facets.ts @@ -226,9 +226,6 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( fieldset { margin-top: 1rem; } - .term-wrapper { - display: block; - } .button { margin-left: auto; margin-right: auto; @@ -383,18 +380,20 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( */ renderTerm(term: FacetTerm) { return html` -
+
- + +
+ ${term.name} + ${term.count + ? html`(${term.count})` + : nothing} +
+
`; } diff --git a/frontend/src/search-radio.ts b/frontend/src/search-radio.ts new file mode 100644 index 00000000..8ceb61b8 --- /dev/null +++ b/frontend/src/search-radio.ts @@ -0,0 +1,112 @@ +import {LitElement, html, css} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; +import {CheckedInputMixin} from './mixins/checked-input'; + +/** + * A custom element that represents a radio. + * + * This component is useful to have state of variable reflected back in the radio, + * overriding updated method. + * @extends {LitElement} + */ +@customElement('searchalicious-radio') +export class SearchaliciousRadio extends CheckedInputMixin(LitElement) { + /** + * The styles for the radio. + * @type {import('lit').CSSResult} + */ + static override styles = css` + .radio-wrapper { + display: flex; + align-items: center; + flex-wrap: wrap; + } + + input[type='radio'] { + cursor: pointer; + position: relative; + flex-shrink: 0; + width: 20px; + height: 20px; + margin-right: 0; + appearance: none; + border: 1px solid var(--searchalicious-radio-color, black); + background-color: transparent; + border-radius: 50%; + } + + input[type='radio']:checked { + } + + input[type='radio']:focus { + outline: 1px solid var(--searchalicious-radio-focus-color, black); + } + + input[type='radio']:checked:after { + position: absolute; + content: ''; + height: 14px; + width: 14px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 50%; + background-color: var(--searchalicious-radio-color, black); + } + + label { + cursor: pointer; + padding-left: 8px; + } + `; + + /** + * Allows or disallows the radio to be unchecked. + */ + @property({type: Boolean, attribute: 'can-be-unchecked'}) + canBeUnchecked = false; + + /** + * Represents the id of the input. + */ + @property({type: String}) + inputId = ''; + + /** + * Allows for the radio to be unchecked. + * @param {Event} e - The event object. + */ + _handleClick() { + if (this.canBeUnchecked && this.checked) { + this.checked = false; + this._dispatchChangeEvent(this.checked, this.name); + } + } + /** + * Renders the radio. + * @returns {import('lit').TemplateResult<1>} - The HTML template for the radio. + */ + override render() { + return html` +
+ + +
+ `; + } +} +declare global { + interface HTMLElementTagNameMap { + 'searchalicious-radio': SearchaliciousRadio; + } +} diff --git a/frontend/src/search-toggle.ts b/frontend/src/search-toggle.ts new file mode 100644 index 00000000..cc7114d0 --- /dev/null +++ b/frontend/src/search-toggle.ts @@ -0,0 +1,122 @@ +import {LitElement, html, css} from 'lit'; +import {customElement} from 'lit/decorators.js'; +import {CheckedInputMixin} from './mixins/checked-input'; + +/** + * A custom element that represents a toggle. + * + * This component is useful to have state of variable reflected back in the toggle, + * overriding updated method. + * @extends {LitElement} + */ +@customElement('searchalicious-toggle') +export class SearchaliciousToggle extends CheckedInputMixin(LitElement) { + /** + * The styles for the toggle. + * input[type='checkbox'] is hidden and the slider is used to represent the toggle. + * We keep input for accessibility. + * @type {import('lit').CSSResult} + */ + static override styles = css` + .toggle-wrapper { + display: flex; + align-items: center; + flex-wrap: wrap; + } + + label { + cursor: pointer; + padding-right: 8px; + } + + .toggle { + cursor: pointer; + position: relative; + display: inline-block; + width: 30px; + height: 17px; + flex-shrink: 0; + } + + .toggle input { + display: none; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var( + --searchalicious-toggle-inactive-background-color, + grey + ); + transition: 0.4s; + border-radius: 17px; + } + + .slider:before { + position: absolute; + content: ''; + height: 13px; + width: 13px; + left: 2px; + bottom: 2px; + background-color: var(--searchalicious-toggle-circle-color, white); + transition: 0.4s; + border-radius: 50%; + } + input[type='checkbox']:checked + .slider { + background-color: var( + --searchalicious-toggle-active-background-color, + black + ); + } + input[type='checkbox']:checked + .slider:before { + transform: translateX(13px); + } + `; + + onClick() { + const inputElement = this.getInputElement(); + if (!inputElement) { + return; + } + inputElement.checked = !inputElement.checked; + this.checked = !this.checked; + this._dispatchChangeEvent(this.checked, this.name); + } + + /** + * Renders the toggle. + * @returns {import('lit').TemplateResult<1>} - The HTML template for the toggle. + */ + override render() { + return html` +
+ +
+ + +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'searchalicious-toggle': SearchaliciousToggle; + } +} diff --git a/frontend/src/utils/enums.ts b/frontend/src/utils/enums.ts index 00cdccd2..27fbda89 100644 --- a/frontend/src/utils/enums.ts +++ b/frontend/src/utils/enums.ts @@ -19,6 +19,7 @@ export enum SearchaliciousEvents { */ export enum BasicEvents { CLICK = 'click', + CHANGE = 'change', } /**