From 2b99daee483179f9d61099abd3b84dc810e3ee70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Mon, 14 Aug 2023 19:33:02 +0300 Subject: [PATCH 01/23] feat(Select): define component skeleton from `bq-input` --- packages/bee-q/src/components.d.ts | 123 ++++++++ .../bee-q/src/components/select/bq-select.tsx | 282 ++++++++++++++++++ .../bee-q/src/components/select/readme.md | 67 +++++ .../src/components/select/scss/bq-select.scss | 154 ++++++++++ .../select/scss/bq-select.variables.scss | 59 ++++ 5 files changed, 685 insertions(+) create mode 100644 packages/bee-q/src/components/select/bq-select.tsx create mode 100644 packages/bee-q/src/components/select/readme.md create mode 100644 packages/bee-q/src/components/select/scss/bq-select.scss create mode 100644 packages/bee-q/src/components/select/scss/bq-select.variables.scss diff --git a/packages/bee-q/src/components.d.ts b/packages/bee-q/src/components.d.ts index 3a40bb5e1..f7e8e53bf 100644 --- a/packages/bee-q/src/components.d.ts +++ b/packages/bee-q/src/components.d.ts @@ -596,6 +596,53 @@ export namespace Components { */ "value"?: string; } + interface BqSelect { + /** + * If true, the Select input will be focused on component render + */ + "autofocus": boolean; + /** + * The clear button aria label + */ + "clearButtonLabel"?: string; + /** + * If true, the clear button won't be displayed + */ + "disableClear"?: boolean; + /** + * Indicates whether the Select input is disabled or not. If `true`, the Select is disabled and cannot be interacted with. + */ + "disabled"?: boolean; + /** + * The ID of the form that the Select input belongs to. + */ + "form"?: string; + /** + * The Select input name. + */ + "name": string; + /** + * The Select input placeholder text value + */ + "placeholder"?: string; + /** + * If true, the Select input cannot be modified. + */ + "readonly"?: boolean; + /** + * Indicates whether or not the Select input is required to be filled out before submitting the form. + */ + "required"?: boolean; + /** + * The validation status of the Select input. + * @remarks This property is used to indicate the validation status of the select input. It can be set to one of the following values: - `'none'`: No validation status is set. - `'error'`: The input has a validation error. - `'warning'`: The input has a validation warning. - `'success'`: The input has passed validation. + */ + "validationStatus": TInputValidation; + /** + * The select input value, it can be used to reset the field to a previous value + */ + "value": TInputValue; + } interface BqSideMenu { /** * It sets a predefined appearance of the side menu @@ -996,6 +1043,10 @@ export interface BqRadioGroupCustomEvent extends CustomEvent { detail: T; target: HTMLBqRadioGroupElement; } +export interface BqSelectCustomEvent extends CustomEvent { + detail: T; + target: HTMLBqSelectElement; +} export interface BqSideMenuCustomEvent extends CustomEvent { detail: T; target: HTMLBqSideMenuElement; @@ -1146,6 +1197,12 @@ declare global { prototype: HTMLBqRadioGroupElement; new (): HTMLBqRadioGroupElement; }; + interface HTMLBqSelectElement extends Components.BqSelect, HTMLStencilElement { + } + var HTMLBqSelectElement: { + prototype: HTMLBqSelectElement; + new (): HTMLBqSelectElement; + }; interface HTMLBqSideMenuElement extends Components.BqSideMenu, HTMLStencilElement { } var HTMLBqSideMenuElement: { @@ -1238,6 +1295,7 @@ declare global { "bq-panel": HTMLBqPanelElement; "bq-radio": HTMLBqRadioElement; "bq-radio-group": HTMLBqRadioGroupElement; + "bq-select": HTMLBqSelectElement; "bq-side-menu": HTMLBqSideMenuElement; "bq-side-menu-item": HTMLBqSideMenuItemElement; "bq-slider": HTMLBqSliderElement; @@ -1898,6 +1956,69 @@ declare namespace LocalJSX { */ "value"?: string; } + interface BqSelect { + /** + * If true, the Select input will be focused on component render + */ + "autofocus"?: boolean; + /** + * The clear button aria label + */ + "clearButtonLabel"?: string; + /** + * If true, the clear button won't be displayed + */ + "disableClear"?: boolean; + /** + * Indicates whether the Select input is disabled or not. If `true`, the Select is disabled and cannot be interacted with. + */ + "disabled"?: boolean; + /** + * The ID of the form that the Select input belongs to. + */ + "form"?: string; + /** + * The Select input name. + */ + "name": string; + /** + * Callback handler emitted when the Select input loses focus + */ + "onBqBlur"?: (event: BqSelectCustomEvent) => void; + /** + * Callback handler emitted when the selected value has changed and the Select input loses focus + */ + "onBqChange"?: (event: BqSelectCustomEvent<{ value: string | number | string[]; el: HTMLBqSelectElement }>) => void; + /** + * Callback handler emitted when the selected value has been cleared + */ + "onBqClear"?: (event: BqSelectCustomEvent) => void; + /** + * Callback handler emitted when the Select input has received focus + */ + "onBqFocus"?: (event: BqSelectCustomEvent) => void; + /** + * The Select input placeholder text value + */ + "placeholder"?: string; + /** + * If true, the Select input cannot be modified. + */ + "readonly"?: boolean; + /** + * Indicates whether or not the Select input is required to be filled out before submitting the form. + */ + "required"?: boolean; + /** + * The validation status of the Select input. + * @remarks This property is used to indicate the validation status of the select input. It can be set to one of the following values: - `'none'`: No validation status is set. - `'error'`: The input has a validation error. - `'warning'`: The input has a validation warning. - `'success'`: The input has passed validation. + */ + "validationStatus"?: TInputValidation; + /** + * The select input value, it can be used to reset the field to a previous value + */ + "value"?: TInputValue; + } interface BqSideMenu { /** * It sets a predefined appearance of the side menu @@ -2308,6 +2429,7 @@ declare namespace LocalJSX { "bq-panel": BqPanel; "bq-radio": BqRadio; "bq-radio-group": BqRadioGroup; + "bq-select": BqSelect; "bq-side-menu": BqSideMenu; "bq-side-menu-item": BqSideMenuItem; "bq-slider": BqSlider; @@ -2352,6 +2474,7 @@ declare module "@stencil/core" { "bq-panel": LocalJSX.BqPanel & JSXBase.HTMLAttributes; "bq-radio": LocalJSX.BqRadio & JSXBase.HTMLAttributes; "bq-radio-group": LocalJSX.BqRadioGroup & JSXBase.HTMLAttributes; + "bq-select": LocalJSX.BqSelect & JSXBase.HTMLAttributes; "bq-side-menu": LocalJSX.BqSideMenu & JSXBase.HTMLAttributes; "bq-side-menu-item": LocalJSX.BqSideMenuItem & JSXBase.HTMLAttributes; "bq-slider": LocalJSX.BqSlider & JSXBase.HTMLAttributes; diff --git a/packages/bee-q/src/components/select/bq-select.tsx b/packages/bee-q/src/components/select/bq-select.tsx new file mode 100644 index 000000000..d8e5560bb --- /dev/null +++ b/packages/bee-q/src/components/select/bq-select.tsx @@ -0,0 +1,282 @@ +import { Component, Element, Event, EventEmitter, h, Prop, State, Watch } from '@stencil/core'; + +import { hasSlotContent, isDefined, isHTMLElement } from '../../shared/utils'; +import { TInputValidation, TInputValue } from '../input/bq-input.types'; + +@Component({ + tag: 'bq-select', + styleUrl: './scss/bq-select.scss', + shadow: true, +}) +export class BqSelect { + // Own Properties + // ==================== + + private helperTextElem?: HTMLElement; + private inputElem?: HTMLInputElement; + private labelElem?: HTMLElement; + private prefixElem?: HTMLElement; + private suffixElem?: HTMLElement; + + private fallbackInputId = 'input'; + + // Reference to host HTML element + // =================================== + + @Element() el!: HTMLBqSelectElement; + + // State() variables + // Inlined decorator, alphabetical order + // ======================================= + + @State() hasHelperText = false; + @State() hasLabel = false; + @State() hasPrefix = false; + @State() hasSuffix = false; + @State() hasValue = false; + + // Public Property API + // ======================== + + /** If true, the Select input will be focused on component render */ + @Prop({ reflect: true }) autofocus: boolean; + + /** The clear button aria label */ + @Prop({ reflect: true }) clearButtonLabel? = 'Clear value'; + + /** + * Indicates whether the Select input is disabled or not. + * If `true`, the Select is disabled and cannot be interacted with. + */ + @Prop({ mutable: true }) disabled?: boolean = false; + + /** If true, the clear button won't be displayed */ + @Prop({ reflect: true }) disableClear? = false; + + /** The ID of the form that the Select input belongs to. */ + @Prop({ reflect: true }) form?: string; + + /** The Select input name. */ + @Prop({ reflect: true }) name!: string; + + /** The Select input placeholder text value */ + @Prop({ reflect: true }) placeholder?: string; + + /** If true, the Select input cannot be modified. */ + @Prop({ reflect: true }) readonly?: boolean; + + /** Indicates whether or not the Select input is required to be filled out before submitting the form. */ + @Prop({ reflect: true }) required?: boolean; + + /** + * The validation status of the Select input. + * + * @remarks + * This property is used to indicate the validation status of the select input. It can be set to one of the following values: + * - `'none'`: No validation status is set. + * - `'error'`: The input has a validation error. + * - `'warning'`: The input has a validation warning. + * - `'success'`: The input has passed validation. + */ + @Prop({ reflect: true }) validationStatus: TInputValidation = 'none'; + + /** The select input value, it can be used to reset the field to a previous value */ + @Prop({ reflect: true, mutable: true }) value: TInputValue; + + // Prop lifecycle events + // ======================= + + @Watch('value') + handleValueChange() { + if (Array.isArray(this.value)) { + this.hasValue = this.value.some((val) => val.length > 0); + return; + } + + this.hasValue = isDefined(this.value); + } + + // Events section + // Requires JSDocs for public API documentation + // ============================================== + + /** Callback handler emitted when the Select input loses focus */ + @Event() bqBlur!: EventEmitter; + + /** Callback handler emitted when the selected value has changed and the Select input loses focus */ + @Event() bqChange!: EventEmitter<{ value: string | number | string[]; el: HTMLBqSelectElement }>; + + /** Callback handler emitted when the selected value has been cleared */ + @Event() bqClear!: EventEmitter; + + /** Callback handler emitted when the Select input has received focus */ + @Event() bqFocus!: EventEmitter; + + // Component lifecycle events + // Ordered by their natural call order + // ===================================== + + componentDidLoad() { + this.handleValueChange(); + } + + // Listeners + // ============== + + // Public methods API + // These methods are exposed on the host element. + // Always use two lines. + // Public Methods must be async. + // Requires JSDocs for public API documentation. + // =============================================== + + // Local methods + // Internal business logic. + // These methods cannot be called from the host element. + // ======================================================= + + private handleBlur = () => { + if (this.disabled) return; + + this.bqBlur.emit(this.el); + }; + + private handleFocus = () => { + if (this.disabled) return; + + this.bqFocus.emit(this.el); + }; + + private handleChange = (ev: Event) => { + if (this.disabled) return; + + if (!isHTMLElement(ev.target, 'input')) return; + this.value = ev.target.value; + + this.bqChange.emit({ value: this.value, el: this.el }); + }; + + private handleClearClick = (ev: CustomEvent) => { + if (this.disabled) return; + + this.inputElem.value = ''; + this.value = this.inputElem.value; + + this.bqClear.emit(this.el); + this.bqChange.emit({ value: this.value, el: this.el }); + this.inputElem.focus(); + + ev.stopPropagation(); + }; + + private handleLabelSlotChange = () => { + this.hasLabel = hasSlotContent(this.labelElem); + }; + + private handlePrefixSlotChange = () => { + this.hasPrefix = hasSlotContent(this.prefixElem); + }; + + private handleSuffixSlotChange = () => { + this.hasSuffix = hasSlotContent(this.suffixElem); + }; + + private handleHelperTextSlotChange = () => { + this.hasHelperText = hasSlotContent(this.helperTextElem); + }; + + // render() function + // Always the last one in the class. + // =================================== + + render() { + return ( +
+ {/* Label */} + + {/* Input control group */} +
+ {/* Prefix */} + (this.prefixElem = spanElem)} + part="prefix" + > + + + {/* HTML Input */} + (this.inputElem = inputElem)} + readOnly={true} + required={this.required} + type="text" + value={this.value} + part="input" + // Events + onBlur={this.handleBlur} + onChange={this.handleChange} + onFocus={this.handleFocus} + /> + {/* Clear Button */} + {this.hasValue && !this.disabled && !this.disableClear && ( + // The clear button will be visible as long as the input has a value + // and the parent group is hovered or has focus-within + + )} + {/* Suffix */} + (this.suffixElem = spanElem)} + part="suffix" + > + + + + +
+ {/* Helper text */} +
(this.helperTextElem = divElem)} + part="helper-text" + > + +
+
+ ); + } +} diff --git a/packages/bee-q/src/components/select/readme.md b/packages/bee-q/src/components/select/readme.md new file mode 100644 index 000000000..949896f8d --- /dev/null +++ b/packages/bee-q/src/components/select/readme.md @@ -0,0 +1,67 @@ +# bq-select + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | --------------- | +| `autofocus` | `autofocus` | If true, the Select input will be focused on component render | `boolean` | `undefined` | +| `clearButtonLabel` | `clear-button-label` | The clear button aria label | `string` | `'Clear value'` | +| `disableClear` | `disable-clear` | If true, the clear button won't be displayed | `boolean` | `false` | +| `disabled` | `disabled` | Indicates whether the Select input is disabled or not. If `true`, the Select is disabled and cannot be interacted with. | `boolean` | `false` | +| `form` | `form` | The ID of the form that the Select input belongs to. | `string` | `undefined` | +| `name` _(required)_ | `name` | The Select input name. | `string` | `undefined` | +| `placeholder` | `placeholder` | The Select input placeholder text value | `string` | `undefined` | +| `readonly` | `readonly` | If true, the Select input cannot be modified. | `boolean` | `undefined` | +| `required` | `required` | Indicates whether or not the Select input is required to be filled out before submitting the form. | `boolean` | `undefined` | +| `validationStatus` | `validation-status` | The validation status of the Select input. | `"error" \| "none" \| "success" \| "warning"` | `'none'` | +| `value` | `value` | The select input value, it can be used to reset the field to a previous value | `number \| string \| string[]` | `undefined` | + + +## Events + +| Event | Description | Type | +| ---------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| `bqBlur` | Callback handler emitted when the Select input loses focus | `CustomEvent` | +| `bqChange` | Callback handler emitted when the selected value has changed and the Select input loses focus | `CustomEvent<{ value: string \| number \| string[]; el: HTMLBqSelectElement; }>` | +| `bqClear` | Callback handler emitted when the selected value has been cleared | `CustomEvent` | +| `bqFocus` | Callback handler emitted when the Select input has received focus | `CustomEvent` | + + +## Shadow Parts + +| Part | Description | +| --------------- | ----------- | +| `"base"` | | +| `"clear-btn"` | | +| `"control"` | | +| `"helper-text"` | | +| `"input"` | | +| `"label"` | | +| `"prefix"` | | +| `"suffix"` | | + + +## Dependencies + +### Depends on + +- [bq-button](../button) +- [bq-icon](../icon) + +### Graph +```mermaid +graph TD; + bq-select --> bq-button + bq-select --> bq-icon + bq-button --> bq-icon + style bq-select fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/bee-q/src/components/select/scss/bq-select.scss b/packages/bee-q/src/components/select/scss/bq-select.scss new file mode 100644 index 000000000..1c5aefd2f --- /dev/null +++ b/packages/bee-q/src/components/select/scss/bq-select.scss @@ -0,0 +1,154 @@ +/* -------------------------------------------------------------------------- */ +/* Select styles */ +/* -------------------------------------------------------------------------- */ + +@import './bq-select.variables'; + +:host { + @apply block w-full; +} + +/* -------------------------------------------------------------------------- */ +/* Label and Helper text */ +/* -------------------------------------------------------------------------- */ + +.bq-select--label { + @apply mb-[--bq-select--label-margin-bottom] flex flex-grow items-center gap-[--var(--bq-select--gap-label)]; + @apply text-[length:--bq-select--label-text-size] text-[color:--bq-select--label-text-color]; +} + +.bq-select--helper-text { + @apply mt-[--bq-select--helper-margin-top] text-[length:--bq-select--helper-text-size] text-[color:--bq-select--helper-text-color]; +} + +.bq-select--helper-text.validation-error { + @apply text-text-danger; +} + +.bq-select--helper-text.validation-success { + @apply text-text-success; +} + +.bq-select--helper-text.validation-warning { + @apply text-text-warning; +} + +/* -------------------------------------------------------------------------- */ +/* Select input group control */ +/* -------------------------------------------------------------------------- */ + +.bq-select--control { + @apply flex w-full items-center transition-[border-color,box-shadow]; + // Border + @apply rounded-[--bq-select--border-radius] border-[length:--bq-select--border-width] border-[color:--bq-select--border-color]; + // Padding + @apply py-[--bq-select--paddingY] pe-[--bq-select--pading-end] ps-[--bq-select--pading-start]; + // Text + @apply text-[length:--bq-select--text-size] text-[color:--bq-select--text-color] placeholder:text-[color:--bq-select--text-placeholder-color]; + // Hover + @apply [&:not(.disabled):not(:focus-within)]:hover:border-[color:--bq-select--border-color-hover]; + + border-style: var(--bq-select--border-style); + + // Focus + &:not(.disabled):focus-within { + --bq-ring-width: 1px; + --bq-ring-offset-width: 0; + --bq-ring-color-focus: var(--bq-select--border-color-focus); + + @apply focus border-[color:--bq-select--border-color-focus]; + } + + // Enable clear button whenever the input group control is hovered or has focus + &:not(.disabled):hover, + &:not(.disabled):focus-within { + .bq-select--control__clear { + @apply inline-block; + } + } +} + +.bq-select--control.disabled { + @apply cursor-not-allowed border-[color:--bq-stroke--tertiary-disabled] bg-ui-secondary-disabled; +} + +/* ------------------------------- Validation ------------------------------- */ + +.bq-select--control.validation-error { + @apply border-stroke-danger [&:not(.disabled):not(:focus-within)]:hover:border-stroke-danger-hover; + + &:not(.disabled):focus-within { + --bq-ring-color-focus: theme('colors.stroke.danger-active'); + @apply border-stroke-danger-active; + } +} + +.bq-select--control.validation-success { + @apply border-stroke-success [&:not(.disabled):not(:focus-within)]:hover:border-stroke-success-hover; + + &:not(.disabled):focus-within { + --bq-ring-color-focus: theme('colors.stroke.success-active'); + @apply border-stroke-success-active; + } +} + +.bq-select--control.validation-warning { + @apply border-stroke-warning [&:not(.disabled):not(:focus-within)]:hover:border-stroke-warning-hover; + + &:not(.disabled):focus-within { + --bq-ring-color-focus: theme('colors.stroke.warning-active'); + @apply border-stroke-warning-active; + } +} + +/* -------------------------------------------------------------------------- */ +/* Native HTML Input */ +/* -------------------------------------------------------------------------- */ + +.bq-select--control__input { + @apply flex-auto cursor-[inherit] appearance-none bg-[inherit] font-[inherit] text-[length:inherit] text-[color:inherit]; + @apply m-0 min-h-[--bq-select--icon-size] min-w-[0] border-none p-0 focus:outline-none focus-visible:outline-none; + + box-shadow: none; + font-weight: inherit; +} + +/* -------------------------------------------------------------------------- */ +/* Clear button */ +/* -------------------------------------------------------------------------- */ + +.bq-select--control__clear::part(button) { + // Since the clear button is inside the input group control, + // we need to reset the focus ring styles + --bq-ring-width: initial; + --bq-ring-offset-width: initial; + --bq-ring-color-focus: initial; + + @apply h-auto rounded-xs border-none p-0; +} + +/* -------------------------------------------------------------------------- */ +/* Prefix and suffix */ +/* -------------------------------------------------------------------------- */ + +.bq-select--control__prefix, +.bq-select--control__suffix { + @apply pointer-events-none flex items-center text-[color:var(--bq-select--text-color)]; +} + +.bq-select--control__prefix { + @apply me-[--bq-select--gap]; +} + +.bq-select--control__suffix { + @apply ms-[--bq-select--gap]; +} + +/* -------------------------------------------------------------------------- */ +/* Slotted and internal icons */ +/* -------------------------------------------------------------------------- */ + +bq-icon, +::slotted(bq-icon) { + --bq-icon--size: var(--bq-select--icon-size) !important; +} diff --git a/packages/bee-q/src/components/select/scss/bq-select.variables.scss b/packages/bee-q/src/components/select/scss/bq-select.variables.scss new file mode 100644 index 000000000..d7917f685 --- /dev/null +++ b/packages/bee-q/src/components/select/scss/bq-select.variables.scss @@ -0,0 +1,59 @@ +/* -------------------------------------------------------------------------- */ +/* Select custom properties */ +/* -------------------------------------------------------------------------- */ + +:host { + /** + * @prop --bq-select--background-color - Select background color + * @prop --bq-select--border-color - Select border color + * @prop --bq-select--border-color-hover - Select border color on hover + * @prop --bq-select--border-color-focus - Select border color on focus + * @prop --bq-select--border-color-disabled - Select border color when disabled + * @prop --bq-select--border-radius - Select border radius + * @prop --bq-select--border-width - Select border width + * @prop --bq-select--border-style - Select border style + * @prop --bq-select--gap - Gap between Select content and prefix/suffix + * @prop --bq-select--helper-margin-top - Helper text margin top + * @prop --bq-select--helper-text-color - Helper text color + * @prop --bq-select--helper-text-size - Helper text size + * @prop --bq-select--icon-size - Icon size to use in prefix/suffix and clear button + * @prop --bq-select--label-margin-bottom - Select label margin bottom + * @prop --bq-select--label-text-color - Select label text color + * @prop --bq-select--label-text-size - Select label text size + * @prop --bq-select--pading-start - Select padding start + * @prop --bq-select--pading-end - Select padding end + * @prop --bq-select--paddingY - Select padding top and bottom + * @prop --bq-select--text-color - Select text color + * @prop --bq-select--text-size - Select text size + * @prop --bq-select--text-placeholder-color - Select placeholder text color + */ + --bq-select--background-color: theme('colors.ui.primary'); + + --bq-select--border-color: theme('colors.stroke.tertiary'); + --bq-select--border-color-hover: theme('colors.stroke.brand-hover'); + --bq-select--border-color-focus: theme('colors.stroke.brand'); + --bq-select--border-color-disabled: theme('colors.stroke.tertiary-disabled'); + --bq-select--border-radius: theme('borderRadius.s'); + --bq-select--border-width: 1px; + --bq-select--border-style: solid; + + --bq-select--gap: theme('spacing.xs'); + + --bq-select--helper-margin-top: theme('spacing.xs'); + --bq-select--helper-text-size: theme('fontSize.s'); + --bq-select--helper-text-color: theme('colors.text.primary'); + + --bq-select--icon-size: 24px; + + --bq-select--label-margin-bottom: theme('spacing.xs'); + --bq-select--label-text-size: theme('fontSize.s'); + --bq-select--label-text-color: theme('colors.text.primary'); + + --bq-select--pading-start: theme('spacing.m'); + --bq-select--pading-end: theme('spacing.m'); + --bq-select--paddingY: theme('spacing.s'); + + --bq-select--text-color: theme('colors.text.primary'); + --bq-select--text-size: theme('fontSize.m'); + --bq-select--text-placeholder-color: theme('colors.text.secondary-disabled'); +} From 84ca668bb2240c41753f5e2033706cc8162aeec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Mon, 14 Aug 2023 19:46:06 +0300 Subject: [PATCH 02/23] feat(Select): add dropdown functionality --- .../bee-q/src/components/select/bq-select.tsx | 147 ++++++++++-------- 1 file changed, 83 insertions(+), 64 deletions(-) diff --git a/packages/bee-q/src/components/select/bq-select.tsx b/packages/bee-q/src/components/select/bq-select.tsx index d8e5560bb..9e2c5da57 100644 --- a/packages/bee-q/src/components/select/bq-select.tsx +++ b/packages/bee-q/src/components/select/bq-select.tsx @@ -3,6 +3,18 @@ import { Component, Element, Event, EventEmitter, h, Prop, State, Watch } from ' import { hasSlotContent, isDefined, isHTMLElement } from '../../shared/utils'; import { TInputValidation, TInputValue } from '../input/bq-input.types'; +/** + * @part base - The component's base wrapper. + * @part button - The native HTML button used under the hood in the clear button. + * @part clear-btn - The clear button. + * @part control - The input control wrapper. + * @part helper-text - The helper text slot container. + * @part input - The native HTML input element used under the hood. + * @part label - The label slot container. + * @part panel - The select panel container + * @part prefix - The prefix slot container. + * @part suffix - The suffix slot container. + */ @Component({ tag: 'bq-select', styleUrl: './scss/bq-select.scss', @@ -201,73 +213,80 @@ export class BqSelect { > - {/* Input control group */} -
- {/* Prefix */} - (this.prefixElem = spanElem)} - part="prefix" + {/* Select dropdown */} + + {/* Input control group */} +
- - - {/* HTML Input */} - (this.inputElem = inputElem)} - readOnly={true} - required={this.required} - type="text" - value={this.value} - part="input" - // Events - onBlur={this.handleBlur} - onChange={this.handleChange} - onFocus={this.handleFocus} - /> - {/* Clear Button */} - {this.hasValue && !this.disabled && !this.disableClear && ( - // The clear button will be visible as long as the input has a value - // and the parent group is hovered or has focus-within - - )} - {/* Suffix */} - (this.suffixElem = spanElem)} - part="suffix" - > - - - - -
+
+
+ + + + {/* Helper text */}
Date: Mon, 14 Aug 2023 19:47:17 +0300 Subject: [PATCH 03/23] docs(Select): update readme files, included `Used by` components --- .../bee-q/src/components/button/readme.md | 2 ++ .../bee-q/src/components/dropdown/readme.md | 5 ++++ packages/bee-q/src/components/icon/readme.md | 2 ++ .../src/components/option-list/readme.md | 13 +++++++++ .../bee-q/src/components/select/readme.md | 27 ++++++++++++------- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/bee-q/src/components/button/readme.md b/packages/bee-q/src/components/button/readme.md index afe3a9605..4a02f83cd 100644 --- a/packages/bee-q/src/components/button/readme.md +++ b/packages/bee-q/src/components/button/readme.md @@ -52,6 +52,7 @@ Buttons are designed for users to take action on a page or a screen. - [bq-dialog](../dialog) - [bq-input](../input) - [bq-notification](../notification) + - [bq-select](../select) ### Depends on @@ -64,6 +65,7 @@ graph TD; bq-dialog --> bq-button bq-input --> bq-button bq-notification --> bq-button + bq-select --> bq-button style bq-button fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/bee-q/src/components/dropdown/readme.md b/packages/bee-q/src/components/dropdown/readme.md index 18233ce7f..e0934fac7 100644 --- a/packages/bee-q/src/components/dropdown/readme.md +++ b/packages/bee-q/src/components/dropdown/readme.md @@ -31,6 +31,10 @@ ## Dependencies +### Used by + + - [bq-select](../select) + ### Depends on - [bq-panel](../panel) @@ -39,6 +43,7 @@ ```mermaid graph TD; bq-dropdown --> bq-panel + bq-select --> bq-dropdown style bq-dropdown fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/bee-q/src/components/icon/readme.md b/packages/bee-q/src/components/icon/readme.md index f873a0046..50aebeec8 100644 --- a/packages/bee-q/src/components/icon/readme.md +++ b/packages/bee-q/src/components/icon/readme.md @@ -40,6 +40,7 @@ Icons are simplified images that graphically explain the meaning of an object on - [bq-dialog](../dialog) - [bq-input](../input) - [bq-notification](../notification) + - [bq-select](../select) - [bq-switch](../switch) - [bq-toast](../toast) @@ -50,6 +51,7 @@ graph TD; bq-dialog --> bq-icon bq-input --> bq-icon bq-notification --> bq-icon + bq-select --> bq-icon bq-switch --> bq-icon bq-toast --> bq-icon style bq-icon fill:#f9f,stroke:#333,stroke-width:4px diff --git a/packages/bee-q/src/components/option-list/readme.md b/packages/bee-q/src/components/option-list/readme.md index 8e806f3af..d46e310c6 100644 --- a/packages/bee-q/src/components/option-list/readme.md +++ b/packages/bee-q/src/components/option-list/readme.md @@ -26,6 +26,19 @@ | `"base"` | The component's internal wrapper. | +## Dependencies + +### Used by + + - [bq-select](../select) + +### Graph +```mermaid +graph TD; + bq-select --> bq-option-list + style bq-option-list fill:#f9f,stroke:#333,stroke-width:4px +``` + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/bee-q/src/components/select/readme.md b/packages/bee-q/src/components/select/readme.md index 949896f8d..dddc4cdb8 100644 --- a/packages/bee-q/src/components/select/readme.md +++ b/packages/bee-q/src/components/select/readme.md @@ -34,30 +34,37 @@ ## Shadow Parts -| Part | Description | -| --------------- | ----------- | -| `"base"` | | -| `"clear-btn"` | | -| `"control"` | | -| `"helper-text"` | | -| `"input"` | | -| `"label"` | | -| `"prefix"` | | -| `"suffix"` | | +| Part | Description | +| --------------- | --------------------------------------------------------------- | +| `"base"` | The component's base wrapper. | +| `"button"` | The native HTML button used under the hood in the clear button. | +| `"clear-btn"` | The clear button. | +| `"control"` | The input control wrapper. | +| `"helper-text"` | The helper text slot container. | +| `"input"` | The native HTML input element used under the hood. | +| `"label"` | The label slot container. | +| `"panel"` | The select panel container | +| `"prefix"` | The prefix slot container. | +| `"suffix"` | The suffix slot container. | ## Dependencies ### Depends on +- [bq-dropdown](../dropdown) - [bq-button](../button) - [bq-icon](../icon) +- [bq-option-list](../option-list) ### Graph ```mermaid graph TD; + bq-select --> bq-dropdown bq-select --> bq-button bq-select --> bq-icon + bq-select --> bq-option-list + bq-dropdown --> bq-panel bq-button --> bq-icon style bq-select fill:#f9f,stroke:#333,stroke-width:4px ``` From 05eadeb0481e03f102533577b8fedc615eb6d7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Mon, 14 Aug 2023 20:57:29 +0300 Subject: [PATCH 04/23] refactor(Select): sync selected option from value --- packages/bee-q/src/components.d.ts | 16 +++-- .../bee-q/src/components/select/bq-select.tsx | 71 +++++++++++++------ .../bee-q/src/components/select/readme.md | 13 ++-- 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/packages/bee-q/src/components.d.ts b/packages/bee-q/src/components.d.ts index f7e8e53bf..f59aa1004 100644 --- a/packages/bee-q/src/components.d.ts +++ b/packages/bee-q/src/components.d.ts @@ -621,6 +621,10 @@ export namespace Components { * The Select input name. */ "name": string; + /** + * If true, the Select panel will be visible. + */ + "open"?: boolean; /** * The Select input placeholder text value */ @@ -1985,10 +1989,6 @@ declare namespace LocalJSX { * Callback handler emitted when the Select input loses focus */ "onBqBlur"?: (event: BqSelectCustomEvent) => void; - /** - * Callback handler emitted when the selected value has changed and the Select input loses focus - */ - "onBqChange"?: (event: BqSelectCustomEvent<{ value: string | number | string[]; el: HTMLBqSelectElement }>) => void; /** * Callback handler emitted when the selected value has been cleared */ @@ -1997,6 +1997,14 @@ declare namespace LocalJSX { * Callback handler emitted when the Select input has received focus */ "onBqFocus"?: (event: BqSelectCustomEvent) => void; + /** + * Callback handler emitted when the selected value has changed + */ + "onBqSelect"?: (event: BqSelectCustomEvent<{ value: string | number | string[]; item: HTMLBqOptionElement }>) => void; + /** + * If true, the Select panel will be visible. + */ + "open"?: boolean; /** * The Select input placeholder text value */ diff --git a/packages/bee-q/src/components/select/bq-select.tsx b/packages/bee-q/src/components/select/bq-select.tsx index 9e2c5da57..11a95f257 100644 --- a/packages/bee-q/src/components/select/bq-select.tsx +++ b/packages/bee-q/src/components/select/bq-select.tsx @@ -1,6 +1,6 @@ import { Component, Element, Event, EventEmitter, h, Prop, State, Watch } from '@stencil/core'; -import { hasSlotContent, isDefined, isHTMLElement } from '../../shared/utils'; +import { getTextContent, hasSlotContent, isDefined } from '../../shared/utils'; import { TInputValidation, TInputValue } from '../input/bq-input.types'; /** @@ -30,7 +30,7 @@ export class BqSelect { private prefixElem?: HTMLElement; private suffixElem?: HTMLElement; - private fallbackInputId = 'input'; + private fallbackInputId = 'select'; // Reference to host HTML element // =================================== @@ -41,6 +41,7 @@ export class BqSelect { // Inlined decorator, alphabetical order // ======================================= + @State() displayValue?: string; @State() hasHelperText = false; @State() hasLabel = false; @State() hasPrefix = false; @@ -71,6 +72,9 @@ export class BqSelect { /** The Select input name. */ @Prop({ reflect: true }) name!: string; + /** If true, the Select panel will be visible. */ + @Prop({ reflect: true }) open?: boolean = false; + /** The Select input placeholder text value */ @Prop({ reflect: true }) placeholder?: string; @@ -100,6 +104,8 @@ export class BqSelect { @Watch('value') handleValueChange() { + this.syncItemsFromValue(); + if (Array.isArray(this.value)) { this.hasValue = this.value.some((val) => val.length > 0); return; @@ -115,15 +121,15 @@ export class BqSelect { /** Callback handler emitted when the Select input loses focus */ @Event() bqBlur!: EventEmitter; - /** Callback handler emitted when the selected value has changed and the Select input loses focus */ - @Event() bqChange!: EventEmitter<{ value: string | number | string[]; el: HTMLBqSelectElement }>; - /** Callback handler emitted when the selected value has been cleared */ @Event() bqClear!: EventEmitter; /** Callback handler emitted when the Select input has received focus */ @Event() bqFocus!: EventEmitter; + /** Callback handler emitted when the selected value has changed */ + @Event() bqSelect!: EventEmitter<{ value: string | number | string[]; item: HTMLBqOptionElement }>; + // Component lifecycle events // Ordered by their natural call order // ===================================== @@ -159,28 +165,24 @@ export class BqSelect { this.bqFocus.emit(this.el); }; - private handleChange = (ev: Event) => { - if (this.disabled) return; - - if (!isHTMLElement(ev.target, 'input')) return; - this.value = ev.target.value; - - this.bqChange.emit({ value: this.value, el: this.el }); - }; - private handleClearClick = (ev: CustomEvent) => { if (this.disabled) return; - this.inputElem.value = ''; - this.value = this.inputElem.value; + this.value = ''; + this.displayValue = ''; this.bqClear.emit(this.el); - this.bqChange.emit({ value: this.value, el: this.el }); this.inputElem.focus(); ev.stopPropagation(); }; + private handleSelect = (ev: CustomEvent<{ value: string | number | string[]; item: HTMLBqOptionElement }>) => { + if (this.disabled) return; + + this.value = ev.detail.value; + }; + private handleLabelSlotChange = () => { this.hasLabel = hasSlotContent(this.labelElem); }; @@ -197,6 +199,28 @@ export class BqSelect { this.hasHelperText = hasSlotContent(this.helperTextElem); }; + private syncItemsFromValue = () => { + const items = this.options; + if (!items.length) return; + + // Sync selected state + this.options.forEach((item: HTMLBqOptionElement) => (item.selected = item.value === this.value)); + // Sync display label + const checkedItem = items.filter((item) => item.value === this.value)[0]; + this.displayValue = checkedItem ? this.getOptionLabel(checkedItem) : ''; + }; + + private getOptionLabel = (item: HTMLBqOptionElement) => { + const slot = item.shadowRoot.querySelector('slot:not([name])'); + if (!slot) return; + + return getTextContent(slot as HTMLSlotElement); + }; + + private get options() { + return Array.from(this.el.querySelectorAll('bq-option')); + } + // render() function // Always the last one in the class. // =================================== @@ -237,8 +261,13 @@ export class BqSelect { (this.inputElem = inputElem)} readOnly={true} required={this.required} + spellcheck={false} type="text" - value={this.value} + value={this.displayValue} part="input" // Events onBlur={this.handleBlur} - onChange={this.handleChange} onFocus={this.handleFocus} /> {/* Clear Button */} @@ -283,7 +312,7 @@ export class BqSelect {
- + diff --git a/packages/bee-q/src/components/select/readme.md b/packages/bee-q/src/components/select/readme.md index dddc4cdb8..aa248d4bc 100644 --- a/packages/bee-q/src/components/select/readme.md +++ b/packages/bee-q/src/components/select/readme.md @@ -15,6 +15,7 @@ | `disabled` | `disabled` | Indicates whether the Select input is disabled or not. If `true`, the Select is disabled and cannot be interacted with. | `boolean` | `false` | | `form` | `form` | The ID of the form that the Select input belongs to. | `string` | `undefined` | | `name` _(required)_ | `name` | The Select input name. | `string` | `undefined` | +| `open` | `open` | If true, the Select panel will be visible. | `boolean` | `false` | | `placeholder` | `placeholder` | The Select input placeholder text value | `string` | `undefined` | | `readonly` | `readonly` | If true, the Select input cannot be modified. | `boolean` | `undefined` | | `required` | `required` | Indicates whether or not the Select input is required to be filled out before submitting the form. | `boolean` | `undefined` | @@ -24,12 +25,12 @@ ## Events -| Event | Description | Type | -| ---------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| `bqBlur` | Callback handler emitted when the Select input loses focus | `CustomEvent` | -| `bqChange` | Callback handler emitted when the selected value has changed and the Select input loses focus | `CustomEvent<{ value: string \| number \| string[]; el: HTMLBqSelectElement; }>` | -| `bqClear` | Callback handler emitted when the selected value has been cleared | `CustomEvent` | -| `bqFocus` | Callback handler emitted when the Select input has received focus | `CustomEvent` | +| Event | Description | Type | +| ---------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `bqBlur` | Callback handler emitted when the Select input loses focus | `CustomEvent` | +| `bqClear` | Callback handler emitted when the selected value has been cleared | `CustomEvent` | +| `bqFocus` | Callback handler emitted when the Select input has received focus | `CustomEvent` | +| `bqSelect` | Callback handler emitted when the selected value has changed | `CustomEvent<{ value: string \| number \| string[]; item: HTMLBqOptionElement; }>` | ## Shadow Parts From 3a90ee25c4bbabef53ca0b4829e0992764d018e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Mon, 14 Aug 2023 21:36:53 +0300 Subject: [PATCH 05/23] fix(Select): delegate focus, also after select --- packages/bee-q/src/components/select/bq-select.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/bee-q/src/components/select/bq-select.tsx b/packages/bee-q/src/components/select/bq-select.tsx index 11a95f257..3eeedc070 100644 --- a/packages/bee-q/src/components/select/bq-select.tsx +++ b/packages/bee-q/src/components/select/bq-select.tsx @@ -18,7 +18,9 @@ import { TInputValidation, TInputValue } from '../input/bq-input.types'; @Component({ tag: 'bq-select', styleUrl: './scss/bq-select.scss', - shadow: true, + shadow: { + delegatesFocus: true, + }, }) export class BqSelect { // Own Properties @@ -177,10 +179,11 @@ export class BqSelect { ev.stopPropagation(); }; - private handleSelect = (ev: CustomEvent<{ value: string | number | string[]; item: HTMLBqOptionElement }>) => { + private handleSelect = (ev: CustomEvent<{ value: TInputValue; item: HTMLBqOptionElement }>) => { if (this.disabled) return; this.value = ev.detail.value; + this.inputElem.focus(); }; private handleLabelSlotChange = () => { From 79cd451e1093a986ab8698b29db98b63a451540b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Tue, 15 Aug 2023 13:06:41 +0300 Subject: [PATCH 06/23] fix(Dropdown): accessibility issues regarding arias and role --- .../src/components/dropdown/bq-dropdown.tsx | 16 ++++++++++------ .../components/option-list/bq-option-list.tsx | 6 +++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/bee-q/src/components/dropdown/bq-dropdown.tsx b/packages/bee-q/src/components/dropdown/bq-dropdown.tsx index 46abb8827..0d98019cc 100644 --- a/packages/bee-q/src/components/dropdown/bq-dropdown.tsx +++ b/packages/bee-q/src/components/dropdown/bq-dropdown.tsx @@ -2,6 +2,8 @@ import { h, Component, Element, Prop, Listen } from '@stencil/core'; import { FloatingUIPlacement } from '../../services/interfaces'; +let id = 0; + /** * @part base - The component's internal wrapper. * @part dropdown - The `` element used under the hood to display the dropdown panel @@ -17,6 +19,8 @@ export class BqDropdown { // Own Properties // ==================== + private dropdownPanelId = `bq-dropdown-panel-${++id}`; + // Reference to host HTML element // =================================== @@ -32,6 +36,9 @@ export class BqDropdown { /** Represents the distance (gutter or margin) between the panel and the trigger element. */ @Prop({ reflect: true }) distance?: number = 4; + /** If true, the panel will remain open after a selection is made. */ + @Prop({ reflect: true }) keepOpenOnSelect?: boolean = false; + /** Position of the panel */ @Prop({ reflect: true }) placement?: FloatingUIPlacement = 'bottom-start'; @@ -41,9 +48,6 @@ export class BqDropdown { /** When set, it will override the height of the dropdown panel */ @Prop({ reflect: true }) panelHeight?: string; - /** If true, the panel will remain open after a selection is made. */ - @Prop({ reflect: true }) keepOpenOnSelect?: boolean = false; - /** Whether the panel should have the same width as the trigger element */ @Prop({ reflect: true }) sameWidth?: boolean = false; @@ -133,7 +137,7 @@ export class BqDropdown { class="bq-dropdown__trigger block" onClick={this.togglePanel} aria-haspopup="true" - aria-expanded={this.open ? 'true' : 'false'} + aria-controls={this.dropdownPanelId} part="trigger" > @@ -141,6 +145,7 @@ export class BqDropdown { {/* PANEL */} diff --git a/packages/bee-q/src/components/option-list/bq-option-list.tsx b/packages/bee-q/src/components/option-list/bq-option-list.tsx index 031b40525..41af24143 100644 --- a/packages/bee-q/src/components/option-list/bq-option-list.tsx +++ b/packages/bee-q/src/components/option-list/bq-option-list.tsx @@ -43,6 +43,10 @@ export class BqOptionList { // Ordered by their natural call order // ===================================== + componentDidLoad() { + this.el.setAttribute('role', 'listbox'); + } + // Listeners // ============== @@ -77,7 +81,7 @@ export class BqOptionList {
From 1ccf09490ca3f684baf0f1f0dcde5b9a0e1b41c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Tue, 15 Aug 2023 13:10:22 +0300 Subject: [PATCH 07/23] fix(Select): aria-* accessibility issues --- packages/bee-q/src/components/select/bq-select.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/bee-q/src/components/select/bq-select.tsx b/packages/bee-q/src/components/select/bq-select.tsx index 3eeedc070..6a4f1fe51 100644 --- a/packages/bee-q/src/components/select/bq-select.tsx +++ b/packages/bee-q/src/components/select/bq-select.tsx @@ -229,10 +229,13 @@ export class BqSelect { // =================================== render() { + const labelId = `bq-select__label-${this.name || this.fallbackInputId}`; + return (
{/* Label */} {/* Select dropdown */} - + {/* Input control group */}
- +
From ac8ec174e4f01638ca165d18140ad2c58ad1e851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Tue, 15 Aug 2023 13:11:02 +0300 Subject: [PATCH 08/23] fix(Selected): supress user click text selection --- packages/bee-q/src/components/select/scss/bq-select.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bee-q/src/components/select/scss/bq-select.scss b/packages/bee-q/src/components/select/scss/bq-select.scss index 1c5aefd2f..c03ba1178 100644 --- a/packages/bee-q/src/components/select/scss/bq-select.scss +++ b/packages/bee-q/src/components/select/scss/bq-select.scss @@ -44,7 +44,7 @@ // Padding @apply py-[--bq-select--paddingY] pe-[--bq-select--pading-end] ps-[--bq-select--pading-start]; // Text - @apply text-[length:--bq-select--text-size] text-[color:--bq-select--text-color] placeholder:text-[color:--bq-select--text-placeholder-color]; + @apply select-none text-[length:--bq-select--text-size] text-[color:--bq-select--text-color] placeholder:text-[color:--bq-select--text-placeholder-color]; // Hover @apply [&:not(.disabled):not(:focus-within)]:hover:border-[color:--bq-select--border-color-hover]; @@ -106,7 +106,7 @@ /* -------------------------------------------------------------------------- */ .bq-select--control__input { - @apply flex-auto cursor-[inherit] appearance-none bg-[inherit] font-[inherit] text-[length:inherit] text-[color:inherit]; + @apply flex-auto cursor-[inherit] select-none appearance-none bg-[inherit] font-[inherit] text-[length:inherit] text-[color:inherit]; @apply m-0 min-h-[--bq-select--icon-size] min-w-[0] border-none p-0 focus:outline-none focus-visible:outline-none; box-shadow: none; From 6f62f0e08323dc4fbc97d5d9f932164e84a2a4af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Tue, 15 Aug 2023 13:46:34 +0300 Subject: [PATCH 09/23] feat(Dropdown): add `bqoOpen` event emitter --- packages/bee-q/src/components.d.ts | 8 ++++++++ packages/bee-q/src/components/dropdown/bq-dropdown.tsx | 10 +++++++++- packages/bee-q/src/components/dropdown/readme.md | 7 +++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/bee-q/src/components.d.ts b/packages/bee-q/src/components.d.ts index f59aa1004..328cb55db 100644 --- a/packages/bee-q/src/components.d.ts +++ b/packages/bee-q/src/components.d.ts @@ -1019,6 +1019,10 @@ export interface BqDialogCustomEvent extends CustomEvent { detail: T; target: HTMLBqDialogElement; } +export interface BqDropdownCustomEvent extends CustomEvent { + detail: T; + target: HTMLBqDropdownElement; +} export interface BqIconCustomEvent extends CustomEvent { detail: T; target: HTMLBqIconElement; @@ -1613,6 +1617,10 @@ declare namespace LocalJSX { * If true, the panel will remain open after a selection is made. */ "keepOpenOnSelect"?: boolean; + /** + * Callback handler to be called when the dropdown panel is opened or closed. + */ + "onBqOpen"?: (event: BqDropdownCustomEvent<{ open: boolean }>) => void; /** * If true, the panel will be visible. */ diff --git a/packages/bee-q/src/components/dropdown/bq-dropdown.tsx b/packages/bee-q/src/components/dropdown/bq-dropdown.tsx index 0d98019cc..e58f8125d 100644 --- a/packages/bee-q/src/components/dropdown/bq-dropdown.tsx +++ b/packages/bee-q/src/components/dropdown/bq-dropdown.tsx @@ -1,4 +1,4 @@ -import { h, Component, Element, Prop, Listen } from '@stencil/core'; +import { h, Component, Element, Prop, Listen, Event, EventEmitter, Watch } from '@stencil/core'; import { FloatingUIPlacement } from '../../services/interfaces'; @@ -60,10 +60,18 @@ export class BqDropdown { // Prop lifecycle events // ======================= + @Watch('open') + onOpenChange() { + this.bqOpen.emit({ open: this.open }); + } + // Events section // Requires JSDocs for public API documentation // ============================================== + /** Callback handler to be called when the dropdown panel is opened or closed. */ + @Event() bqOpen: EventEmitter<{ open: boolean }>; + // Component lifecycle events // Ordered by their natural call order // ===================================== diff --git a/packages/bee-q/src/components/dropdown/readme.md b/packages/bee-q/src/components/dropdown/readme.md index e0934fac7..276b8a707 100644 --- a/packages/bee-q/src/components/dropdown/readme.md +++ b/packages/bee-q/src/components/dropdown/readme.md @@ -19,6 +19,13 @@ | `strategy` | `strategy` | Defines the strategy to position the panel | `"absolute" \| "fixed"` | `'fixed'` | +## Events + +| Event | Description | Type | +| -------- | -------------------------------------------------------------------------- | --------------------------------- | +| `bqOpen` | Callback handler to be called when the dropdown panel is opened or closed. | `CustomEvent<{ open: boolean; }>` | + + ## Shadow Parts | Part | Description | From f1ebe9d85cb6bc98f08a8d4a18683507f358d4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Tue, 15 Aug 2023 13:48:20 +0300 Subject: [PATCH 10/23] feat(Select): animate suffix icon on dropdown panel open/close --- .../bee-q/src/components/select/bq-select.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/bee-q/src/components/select/bq-select.tsx b/packages/bee-q/src/components/select/bq-select.tsx index 6a4f1fe51..d8c64a592 100644 --- a/packages/bee-q/src/components/select/bq-select.tsx +++ b/packages/bee-q/src/components/select/bq-select.tsx @@ -1,4 +1,4 @@ -import { Component, Element, Event, EventEmitter, h, Prop, State, Watch } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, h, Listen, Prop, State, Watch } from '@stencil/core'; import { getTextContent, hasSlotContent, isDefined } from '../../shared/utils'; import { TInputValidation, TInputValue } from '../input/bq-input.types'; @@ -143,6 +143,13 @@ export class BqSelect { // Listeners // ============== + @Listen('bqOpen', { capture: true }) + handleOpenChange(ev: CustomEvent<{ open: boolean }>) { + if (!ev.composedPath().includes(this.el)) return; + + this.open = ev.detail.open; + } + // Public methods API // These methods are exposed on the host element. // Always use two lines. @@ -167,6 +174,13 @@ export class BqSelect { this.bqFocus.emit(this.el); }; + private handleSelect = (ev: CustomEvent<{ value: TInputValue; item: HTMLBqOptionElement }>) => { + if (this.disabled) return; + + this.value = ev.detail.value; + this.inputElem.focus(); + }; + private handleClearClick = (ev: CustomEvent) => { if (this.disabled) return; @@ -179,13 +193,6 @@ export class BqSelect { ev.stopPropagation(); }; - private handleSelect = (ev: CustomEvent<{ value: TInputValue; item: HTMLBqOptionElement }>) => { - if (this.disabled) return; - - this.value = ev.detail.value; - this.inputElem.focus(); - }; - private handleLabelSlotChange = () => { this.hasLabel = hasSlotContent(this.labelElem); }; @@ -309,7 +316,7 @@ export class BqSelect { )} {/* Suffix */} (this.suffixElem = spanElem)} part="suffix" > From 8b8f98a237cc709ea9f139425cc72adf350e86cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Tue, 15 Aug 2023 14:55:56 +0300 Subject: [PATCH 11/23] feat(dropdown): allow disabled and do not open panel if trigger is disabled --- packages/bee-q/src/components.d.ts | 64 +++++++++++++++++++ .../_storybook/bq-dropdown.stories.tsx | 3 + .../src/components/dropdown/bq-dropdown.tsx | 17 ++++- .../bee-q/src/components/dropdown/readme.md | 1 + 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/packages/bee-q/src/components.d.ts b/packages/bee-q/src/components.d.ts index 328cb55db..f0e33f11b 100644 --- a/packages/bee-q/src/components.d.ts +++ b/packages/bee-q/src/components.d.ts @@ -289,6 +289,10 @@ export namespace Components { "titleAlignment"?: TDividerTitleAlignment; } interface BqDropdown { + /** + * If true, the dropdown panel will be visible and won't be shown. + */ + "disabled"?: boolean; /** * Represents the distance (gutter or margin) between the panel and the trigger element. */ @@ -613,10 +617,18 @@ export namespace Components { * Indicates whether the Select input is disabled or not. If `true`, the Select is disabled and cannot be interacted with. */ "disabled"?: boolean; + /** + * Represents the distance (gutter or margin) between the Select panel and the input element. + */ + "distance"?: number; /** * The ID of the form that the Select input belongs to. */ "form"?: string; + /** + * If true, the Select panel will remain open after a selection is made. + */ + "keepOpenOnSelect"?: boolean; /** * The Select input name. */ @@ -625,10 +637,18 @@ export namespace Components { * If true, the Select panel will be visible. */ "open"?: boolean; + /** + * When set, it will override the height of the Select panel. + */ + "panelHeight"?: string; /** * The Select input placeholder text value */ "placeholder"?: string; + /** + * Position of the Select panel + */ + "placement"?: FloatingUIPlacement; /** * If true, the Select input cannot be modified. */ @@ -637,6 +657,18 @@ export namespace Components { * Indicates whether or not the Select input is required to be filled out before submitting the form. */ "required"?: boolean; + /** + * Whether the panel should have the Select same width as the input element + */ + "sameWidth"?: boolean; + /** + * Represents the skidding between the Select panel and the input element. + */ + "skidding"?: number; + /** + * Defines the strategy to position the Select panel + */ + "strategy"?: 'fixed' | 'absolute'; /** * The validation status of the Select input. * @remarks This property is used to indicate the validation status of the select input. It can be set to one of the following values: - `'none'`: No validation status is set. - `'error'`: The input has a validation error. - `'warning'`: The input has a validation warning. - `'success'`: The input has passed validation. @@ -1609,6 +1641,10 @@ declare namespace LocalJSX { "titleAlignment"?: TDividerTitleAlignment; } interface BqDropdown { + /** + * If true, the dropdown panel will be visible and won't be shown. + */ + "disabled"?: boolean; /** * Represents the distance (gutter or margin) between the panel and the trigger element. */ @@ -1985,10 +2021,18 @@ declare namespace LocalJSX { * Indicates whether the Select input is disabled or not. If `true`, the Select is disabled and cannot be interacted with. */ "disabled"?: boolean; + /** + * Represents the distance (gutter or margin) between the Select panel and the input element. + */ + "distance"?: number; /** * The ID of the form that the Select input belongs to. */ "form"?: string; + /** + * If true, the Select panel will remain open after a selection is made. + */ + "keepOpenOnSelect"?: boolean; /** * The Select input name. */ @@ -2013,10 +2057,18 @@ declare namespace LocalJSX { * If true, the Select panel will be visible. */ "open"?: boolean; + /** + * When set, it will override the height of the Select panel. + */ + "panelHeight"?: string; /** * The Select input placeholder text value */ "placeholder"?: string; + /** + * Position of the Select panel + */ + "placement"?: FloatingUIPlacement; /** * If true, the Select input cannot be modified. */ @@ -2025,6 +2077,18 @@ declare namespace LocalJSX { * Indicates whether or not the Select input is required to be filled out before submitting the form. */ "required"?: boolean; + /** + * Whether the panel should have the Select same width as the input element + */ + "sameWidth"?: boolean; + /** + * Represents the skidding between the Select panel and the input element. + */ + "skidding"?: number; + /** + * Defines the strategy to position the Select panel + */ + "strategy"?: 'fixed' | 'absolute'; /** * The validation status of the Select input. * @remarks This property is used to indicate the validation status of the select input. It can be set to one of the following values: - `'none'`: No validation status is set. - `'error'`: The input has a validation error. - `'warning'`: The input has a validation warning. - `'success'`: The input has passed validation. diff --git a/packages/bee-q/src/components/dropdown/_storybook/bq-dropdown.stories.tsx b/packages/bee-q/src/components/dropdown/_storybook/bq-dropdown.stories.tsx index dd29fdd71..b0530df23 100644 --- a/packages/bee-q/src/components/dropdown/_storybook/bq-dropdown.stories.tsx +++ b/packages/bee-q/src/components/dropdown/_storybook/bq-dropdown.stories.tsx @@ -13,6 +13,7 @@ const meta: Meta = { }, }, argTypes: { + disabled: { control: 'boolean' }, distance: { control: 'number' }, placement: { control: 'select', @@ -43,6 +44,7 @@ const meta: Meta = { trigger: { control: 'text', table: { disable: true } }, }, args: { + disabled: false, distance: 4, placement: 'bottom-start', open: false, @@ -60,6 +62,7 @@ type Story = StoryObj; const Template = (args: Args) => html` { + // Don't toggle the panel if the component is disabled or the trigger element is disabled + if (this.disabled || this.triggerElem?.hasAttribute('disabled')) return; + this.open = !this.open; }; @@ -140,12 +151,12 @@ export class BqDropdown { return (
- {/* TRIGGER ELEMENT */} + {/* TRIGGER CONTAINER */}
diff --git a/packages/bee-q/src/components/dropdown/readme.md b/packages/bee-q/src/components/dropdown/readme.md index 276b8a707..71f4012c5 100644 --- a/packages/bee-q/src/components/dropdown/readme.md +++ b/packages/bee-q/src/components/dropdown/readme.md @@ -9,6 +9,7 @@ | Property | Attribute | Description | Type | Default | | ------------------ | --------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | +| `disabled` | `disabled` | If true, the dropdown panel will be visible and won't be shown. | `boolean` | `false` | | `distance` | `distance` | Represents the distance (gutter or margin) between the panel and the trigger element. | `number` | `4` | | `keepOpenOnSelect` | `keep-open-on-select` | If true, the panel will remain open after a selection is made. | `boolean` | `false` | | `open` | `open` | If true, the panel will be visible. | `boolean` | `false` | From 14b18f6488d68de5cbcac840c10ae6f4a28e4196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Tue, 15 Aug 2023 14:57:19 +0300 Subject: [PATCH 12/23] feat(Select): add panel properties --- .../bee-q/src/components/select/bq-select.tsx | 36 ++++++++++++++++++- .../bee-q/src/components/select/readme.md | 35 ++++++++++-------- .../src/components/select/scss/bq-select.scss | 2 +- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/packages/bee-q/src/components/select/bq-select.tsx b/packages/bee-q/src/components/select/bq-select.tsx index d8c64a592..c9145010c 100644 --- a/packages/bee-q/src/components/select/bq-select.tsx +++ b/packages/bee-q/src/components/select/bq-select.tsx @@ -1,5 +1,6 @@ import { Component, Element, Event, EventEmitter, h, Listen, Prop, State, Watch } from '@stencil/core'; +import { FloatingUIPlacement } from '../../services/interfaces'; import { getTextContent, hasSlotContent, isDefined } from '../../shared/utils'; import { TInputValidation, TInputValue } from '../input/bq-input.types'; @@ -68,24 +69,45 @@ export class BqSelect { /** If true, the clear button won't be displayed */ @Prop({ reflect: true }) disableClear? = false; + /** Represents the distance (gutter or margin) between the Select panel and the input element. */ + @Prop({ reflect: true }) distance?: number = 8; + /** The ID of the form that the Select input belongs to. */ @Prop({ reflect: true }) form?: string; + /** If true, the Select panel will remain open after a selection is made. */ + @Prop({ reflect: true }) keepOpenOnSelect?: boolean = false; + /** The Select input name. */ @Prop({ reflect: true }) name!: string; /** If true, the Select panel will be visible. */ @Prop({ reflect: true }) open?: boolean = false; + /** When set, it will override the height of the Select panel. */ + @Prop({ reflect: true }) panelHeight?: string; + /** The Select input placeholder text value */ @Prop({ reflect: true }) placeholder?: string; + /** Position of the Select panel */ + @Prop({ reflect: true }) placement?: FloatingUIPlacement = 'bottom'; + /** If true, the Select input cannot be modified. */ @Prop({ reflect: true }) readonly?: boolean; /** Indicates whether or not the Select input is required to be filled out before submitting the form. */ @Prop({ reflect: true }) required?: boolean; + /** Whether the panel should have the Select same width as the input element */ + @Prop({ reflect: true }) sameWidth?: boolean = true; + + /** Represents the skidding between the Select panel and the input element. */ + @Prop({ reflect: true }) skidding?: number = 0; + + /** Defines the strategy to position the Select panel */ + @Prop({ reflect: true }) strategy?: 'fixed' | 'absolute' = 'fixed'; + /** * The validation status of the Select input. * @@ -251,7 +273,19 @@ export class BqSelect { {/* Select dropdown */} - + {/* Input control group */}
Date: Tue, 15 Aug 2023 15:03:02 +0300 Subject: [PATCH 13/23] fix(Option): avoid bubbling for focus and blur events --- packages/bee-q/src/components/option/bq-option.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bee-q/src/components/option/bq-option.tsx b/packages/bee-q/src/components/option/bq-option.tsx index 92bf222dd..0d301a5a3 100644 --- a/packages/bee-q/src/components/option/bq-option.tsx +++ b/packages/bee-q/src/components/option/bq-option.tsx @@ -52,10 +52,10 @@ export class BqOption { // ============================================== /** Handler to be called when item loses focus */ - @Event() bqBlur: EventEmitter; + @Event({ bubbles: false }) bqBlur: EventEmitter; /** Handler to be called when item is focused */ - @Event() bqFocus: EventEmitter; + @Event({ bubbles: false }) bqFocus: EventEmitter; /** Handler to be called when item is clicked */ @Event() bqClick: EventEmitter; From 2dfb9c33e73c7ab2cf68edb7179f012d57b0f629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Tue, 15 Aug 2023 15:09:45 +0300 Subject: [PATCH 14/23] feat(Angular): include `bq-select` in the Angular Value Accessor Bindings --- packages/bee-q/src/tools/angular-value-accessor-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bee-q/src/tools/angular-value-accessor-config.ts b/packages/bee-q/src/tools/angular-value-accessor-config.ts index a2ed6d5c8..859108f7c 100644 --- a/packages/bee-q/src/tools/angular-value-accessor-config.ts +++ b/packages/bee-q/src/tools/angular-value-accessor-config.ts @@ -8,7 +8,7 @@ export const angularValueAccessorBindings: ValueAccessorConfig[] = [ type: 'boolean', }, { - elementSelectors: ['bq-input', 'bq-radio-group', 'bq-slider'], + elementSelectors: ['bq-input', 'bq-radio-group', 'bq-select', 'bq-slider'], event: 'bqChange', targetAttr: 'value', type: 'text', From 82ad890411536e155d5fb993378fe6ae8318b6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Tue, 15 Aug 2023 15:10:37 +0300 Subject: [PATCH 15/23] docs(Select): update Storybook examples and .mdx docs --- .../select/_storybook/bq-select.mdx | 24 ++ .../select/_storybook/bq-select.stories.tsx | 219 ++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 packages/bee-q/src/components/select/_storybook/bq-select.mdx create mode 100644 packages/bee-q/src/components/select/_storybook/bq-select.stories.tsx diff --git a/packages/bee-q/src/components/select/_storybook/bq-select.mdx b/packages/bee-q/src/components/select/_storybook/bq-select.mdx new file mode 100644 index 000000000..ef0b6f63e --- /dev/null +++ b/packages/bee-q/src/components/select/_storybook/bq-select.mdx @@ -0,0 +1,24 @@ +import { ArgTypes, Title, Subtitle } from '@storybook/addon-docs'; + +Select + +The Select Component is a UI element used to allow users to select from a list of options. +Select components are commonly used in forms and other applications to provide a convenient way for users to select a value from a list of options. + +Usage + +- Allowing users to select from a list of options, such as a list of countries, a list of colors, or a list of dates. +- Filtering or narrowing down a list of options, such as by searching or selecting a category, to make it easier for users to find the option they need. +- Providing a compact and efficient way to select a value from a large list of options, without taking up too much space on the screen. + +👍 When to use + +Use the Select component when: + +- Choosing from a predefined list: Select components are ideal for situations where users need to choose a single option from a predetermined list of options. +- Consistent option selection: If the options available are consistent across users or use cases, a select component provides a standardized and familiar interface for users to make their selections. +- Limited screen space: If screen real estate is limited, a select component can be a space-efficient way of presenting a list of options without cluttering the interface. + +Properties + + diff --git a/packages/bee-q/src/components/select/_storybook/bq-select.stories.tsx b/packages/bee-q/src/components/select/_storybook/bq-select.stories.tsx new file mode 100644 index 000000000..1d303ad2a --- /dev/null +++ b/packages/bee-q/src/components/select/_storybook/bq-select.stories.tsx @@ -0,0 +1,219 @@ +import type { Args, Meta, StoryObj } from '@storybook/web-components'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { html, nothing } from 'lit-html'; + +import mdx from './bq-select.mdx'; +import { INPUT_VALIDATION } from '../../input/bq-input.types'; + +const meta: Meta = { + title: 'Components/Select', + component: 'bq-select', + parameters: { + docs: { + page: mdx, + }, + }, + argTypes: { + autofocus: { control: 'boolean' }, + 'clear-button-label': { control: 'text' }, + 'disable-clear': { control: 'boolean' }, + distance: { control: 'number' }, + disabled: { control: 'boolean' }, + form: { control: 'text' }, + 'keep-open-on-select': { control: 'boolean' }, + name: { control: 'text' }, + open: { control: 'boolean' }, + 'panel-height': { control: 'text' }, + placement: { + control: 'select', + options: [ + 'top', + 'top-start', + 'top-end', + 'bottom', + 'bottom-start', + 'bottom-end', + 'right', + 'right-start', + 'right-end', + 'left', + 'left-start', + 'left-end', + ], + }, + placeholder: { control: 'text' }, + readonly: { control: 'boolean' }, + required: { control: 'boolean' }, + 'same-width': { control: 'boolean' }, + skidding: { control: 'number' }, + strategy: { control: 'select', options: ['fixed', 'absolute'] }, + 'validation-status': { control: 'select', options: [...INPUT_VALIDATION] }, + value: { control: 'text' }, + // Events + bqBlur: { action: 'bqBlur' }, + bqClear: { action: 'bqClear' }, + bqFocus: { action: 'bqFocus' }, + bqSelect: { action: 'bqSelect' }, + // Not part of the public API, so we don't want to expose it in the docs + noLabel: { control: 'bolean', table: { disable: true } }, + hasLabelTooltip: { control: 'bolean', table: { disable: true } }, + noHelperText: { control: 'bolean', table: { disable: true } }, + optionalLabel: { control: 'bolean', table: { disable: true } }, + prefix: { control: 'bolean', table: { disable: true } }, + suffix: { control: 'bolean', table: { disable: true } }, + options: { control: 'text', table: { disable: true } }, + }, + args: { + autofocus: false, + 'clear-button-label': 'Clear value', + 'disable-clear': false, + distance: 8, + disabled: false, + form: undefined, + 'keep-open-on-select': false, + name: 'bq-select', + open: false, + 'panel-height': undefined, + placement: 'bottom', + placeholder: 'Placeholder', + 'same-width': false, + skidding: 0, + strategy: 'absolute', + readonly: false, + required: false, + 'validation-status': 'none', + value: undefined, + // Not part of the public API, so we don't want to expose it in the docs + options: html` + Option 1 + Option 2 + Option 3 + Option 4 + Option 5 + `, + }, +}; +export default meta; + +type Story = StoryObj; + +const Template = (args: Args) => { + const tooltipTemplate = args.hasLabelTooltip + ? html` + + + You can provide more context detail by adding a tooltip to the label. + + ` + : nothing; + const labelTemplate = html` + + `; + const label = !args.optionalLabel + ? labelTemplate + : html` +
+ ${labelTemplate} + Optional +
+ `; + const style = args.hasLabelTooltip + ? html` + + ` + : nothing; + + return html` + ${style} + + ${!args.noLabel ? label : nothing} + ${args.prefix ? html`` : nothing} + ${args.suffix ? html`` : nothing} + ${!args.noHelperText + ? html` + + + Helper text + + ` + : nothing} + ${args.options} + + `; +}; + +export const Default: Story = { + render: Template, +}; + +export const Open: Story = { + render: Template, + args: { + autofocus: true, + open: true, + }, +}; + +export const InitialValue: Story = { + render: Template, + args: { + value: '2', + }, +}; + +export const Validation: Story = { + render: (args) => html` +
+ + ${Template({ ...args, 'validation-status': 'error', value: 1 })} + + ${Template({ ...args, 'validation-status': 'warning', value: 3 })} + + ${Template({ ...args, 'validation-status': 'success', value: 5 })} +
+ `, +}; + +export const Tooltip: Story = { + name: 'Label with "Info tooltip"', + render: Template, + args: { + hasLabelTooltip: true, + optionalLabel: true, + prefix: true, + suffix: true, + }, + parameters: { + layout: 'centered', + }, +}; From 8fa887e836f51187bb885fbb86c7e022cc33b8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Wed, 16 Aug 2023 12:31:13 +0300 Subject: [PATCH 16/23] test(Select): add e2e tests --- .../option/__tests__/bq-option.e2e.ts | 70 +++++---- .../select/__tests__/bq-select.e2e.ts | 141 ++++++++++++++++++ 2 files changed, 175 insertions(+), 36 deletions(-) create mode 100644 packages/bee-q/src/components/select/__tests__/bq-select.e2e.ts diff --git a/packages/bee-q/src/components/option/__tests__/bq-option.e2e.ts b/packages/bee-q/src/components/option/__tests__/bq-option.e2e.ts index 730d5dd4a..1fb0f0164 100644 --- a/packages/bee-q/src/components/option/__tests__/bq-option.e2e.ts +++ b/packages/bee-q/src/components/option/__tests__/bq-option.e2e.ts @@ -2,42 +2,43 @@ import { newE2EPage } from '@stencil/core/testing'; describe('bq-option', () => { it('should render', async () => { - const page = await newE2EPage(); - await page.setContent(''); - + const page = await newE2EPage({ + html: 'Option label', + }); const element = await page.find('bq-option'); expect(element).toHaveClass('hydrated'); }); it('should have shadow root', async () => { - const page = await newE2EPage(); - await page.setContent(''); - + const page = await newE2EPage({ + html: 'Option label', + }); const element = await page.find('bq-option'); expect(element.shadowRoot).not.toBeNull(); }); it('should display text', async () => { - const page = await newE2EPage(); - await page.setContent('Option 1'); - + const text = 'Option label'; + const page = await newE2EPage({ + html: `${text}`, + }); const element = await page.find('bq-option'); - expect(element).toEqualText('Option 1'); + expect(element).toEqualText(text); }); it('should trigger bqClick', async () => { - const page = await newE2EPage(); - await page.setContent('Menu item label'); + const page = await newE2EPage({ + html: 'Option label', + }); const bqFocus = await page.spyOnEvent('bqFocus'); const bqBlur = await page.spyOnEvent('bqBlur'); const bqClick = await page.spyOnEvent('bqClick'); const element = await page.find('bq-option'); - await element.click(); expect(bqFocus).toHaveReceivedEventTimes(1); @@ -46,14 +47,9 @@ describe('bq-option', () => { }); it('should be keyboard accessible', async () => { - const page = await newE2EPage(); - await page.setContent(` - - - Option 1 - - - `); + const page = await newE2EPage({ + html: 'Option label', + }); const bqFocus = await page.spyOnEvent('bqFocus'); const bqBlur = await page.spyOnEvent('bqBlur'); @@ -66,8 +62,9 @@ describe('bq-option', () => { }); it('should handle Enter', async () => { - const page = await newE2EPage(); - await page.setContent('Option 1'); + const page = await newE2EPage({ + html: 'Option label', + }); const bqFocus = await page.spyOnEvent('bqFocus'); const bqBlur = await page.spyOnEvent('bqBlur'); @@ -84,15 +81,14 @@ describe('bq-option', () => { }); it('should handle `disabled` property', async () => { - const page = await newE2EPage(); - await page.setContent('Option 1'); - + const page = await newE2EPage({ + html: 'Option label', + }); const bqFocus = await page.spyOnEvent('bqFocus'); const bqBlur = await page.spyOnEvent('bqBlur'); const bqClick = await page.spyOnEvent('bqClick'); const element = await page.find('bq-option'); - element.click(); await page.waitForChanges(); @@ -122,13 +118,14 @@ describe('bq-option', () => { }); it('should render suffix element', async () => { - const page = await newE2EPage(); - await page.setContent(` - - Option label - Suffix - - `); + const page = await newE2EPage({ + html: ` + + Option label + Suffix + + `, + }); const suffixText = await page.$eval('bq-option', (element) => { const slotElement = element.shadowRoot.querySelector('slot[name="suffix"]'); @@ -141,8 +138,9 @@ describe('bq-option', () => { }); it('should handle `selected` property', async () => { - const page = await newE2EPage(); - await page.setContent('Option 1'); + const page = await newE2EPage({ + html: 'Option 1', + }); const bqOption = await page.find('bq-option >>> div'); diff --git a/packages/bee-q/src/components/select/__tests__/bq-select.e2e.ts b/packages/bee-q/src/components/select/__tests__/bq-select.e2e.ts new file mode 100644 index 000000000..60b92a2e9 --- /dev/null +++ b/packages/bee-q/src/components/select/__tests__/bq-select.e2e.ts @@ -0,0 +1,141 @@ +import { newE2EPage } from '@stencil/core/testing'; + +describe('bq-select', () => { + it('should render', async () => { + const page = await newE2EPage({ + html: ``, + }); + const element = await page.find('bq-select'); + + expect(element).toHaveClass('hydrated'); + }); + + it('should have shadow root', async () => { + const page = await newE2EPage({ + html: ``, + }); + const element = await page.find('bq-select'); + + expect(element.shadowRoot).not.toBeNull(); + }); + + it('should render with default suffix icon', async () => { + const page = await newE2EPage({ + html: ``, + }); + const suffixIconElem = await page.find('bq-select >>> bq-icon[name="caret-down"]'); + + expect(suffixIconElem).not.toBeNull(); + }); + + it('should render with prefix icon', async () => { + const page = await newE2EPage({ + html: ` + + + + `, + }); + const prefixContainerElem = await page.find('bq-select >>> .bq-select--control__prefix'); + expect(prefixContainerElem).not.toHaveClass('hidden'); + }); + + it('should render with label content', async () => { + const page = await newE2EPage({ + html: ` + + + + `, + }); + const labelContainerElem = await page.find('bq-select >>> .bq-select--label'); + + expect(labelContainerElem).not.toHaveClass('hidden'); + }); + + it('should render with helper content', async () => { + const page = await newE2EPage({ + html: ` + + Helper text + + `, + }); + const helperContainerElem = await page.find('bq-select >>> .bq-select--helper-text'); + + expect(helperContainerElem).not.toHaveClass('hidden'); + }); + + it('should render with options', async () => { + const page = await newE2EPage({ + html: ` + + Option 1 + Option 2 + Option 3 + + `, + }); + const optionElems = await page.findAll('bq-select > bq-option'); + + expect(optionElems.length).toEqual(3); + }); + + it('should render with panel options opened', async () => { + const page = await newE2EPage({ + html: ` + + Option 1 + Option 2 + Option 3 + + `, + }); + const selectPanelElem = await page.find('bq-select >>> .bq-select__dropdown >>> .bq-dropdown__panel'); + + expect(selectPanelElem).toHaveAttribute('open'); + }); + + it('should render with selected option', async () => { + const selectedValue = 1; + const page = await newE2EPage({ + html: ` + + Option 1 + Option 2 + Option 3 + + `, + }); + const selectValueElem = await page.find(`bq-select bq-option[value="${selectedValue}"]`); + + expect(selectValueElem).toHaveAttribute('selected'); + }); + + it('should select an option and emit Select event', async () => { + const value = 2; + const page = await newE2EPage({ + html: ` + + Option 1 + Option 2 + Option 3 + + `, + }); + const eventEmitter = await page.spyOnEvent('bqSelect'); + + const selectElem = await page.find('bq-select'); + await selectElem.click(); + await page.waitForChanges(); + + const selectOptionElem = await page.find(`bq-option[value="${value}"]`); + expect(selectOptionElem).not.toHaveAttribute('selected'); + + await selectOptionElem.click(); + await page.waitForChanges(); + + expect(selectOptionElem).toHaveAttribute('selected'); + expect(eventEmitter).toHaveReceivedEventTimes(1); + }); +}); From 05517e344583a030ba43ceedfad84403a6dd6855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Wed, 16 Aug 2023 13:54:16 +0300 Subject: [PATCH 17/23] fix(Select): stop focus and blur event propagation from bq-option(s) --- packages/bee-q/src/components/option/bq-option.tsx | 4 ++-- packages/bee-q/src/components/select/bq-select.tsx | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/bee-q/src/components/option/bq-option.tsx b/packages/bee-q/src/components/option/bq-option.tsx index 0d301a5a3..92bf222dd 100644 --- a/packages/bee-q/src/components/option/bq-option.tsx +++ b/packages/bee-q/src/components/option/bq-option.tsx @@ -52,10 +52,10 @@ export class BqOption { // ============================================== /** Handler to be called when item loses focus */ - @Event({ bubbles: false }) bqBlur: EventEmitter; + @Event() bqBlur: EventEmitter; /** Handler to be called when item is focused */ - @Event({ bubbles: false }) bqFocus: EventEmitter; + @Event() bqFocus: EventEmitter; /** Handler to be called when item is clicked */ @Event() bqClick: EventEmitter; diff --git a/packages/bee-q/src/components/select/bq-select.tsx b/packages/bee-q/src/components/select/bq-select.tsx index c9145010c..cc4aa92d3 100644 --- a/packages/bee-q/src/components/select/bq-select.tsx +++ b/packages/bee-q/src/components/select/bq-select.tsx @@ -1,7 +1,7 @@ import { Component, Element, Event, EventEmitter, h, Listen, Prop, State, Watch } from '@stencil/core'; import { FloatingUIPlacement } from '../../services/interfaces'; -import { getTextContent, hasSlotContent, isDefined } from '../../shared/utils'; +import { getTextContent, hasSlotContent, isDefined, isHTMLElement } from '../../shared/utils'; import { TInputValidation, TInputValue } from '../input/bq-input.types'; /** @@ -172,6 +172,15 @@ export class BqSelect { this.open = ev.detail.open; } + @Listen('bqFocus', { capture: true }) + @Listen('bqBlur', { capture: true }) + stopOptionFocusBlurPropagation(ev: CustomEvent) { + // Stop propagation of focus and blur events coming from the `bq-option` elements + if (isHTMLElement(ev.target, 'bq-select')) return; + + ev.stopPropagation(); + } + // Public methods API // These methods are exposed on the host element. // Always use two lines. From 16e214f6d1da5c039bcbc4e8d7d5b3240ff30dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dabiel=20Gonz=C3=A1lez=20Ramos?= Date: Wed, 16 Aug 2023 14:03:38 +0300 Subject: [PATCH 18/23] style(Select): refactor CSS classes to use BEM --- .../bee-q/src/components/select/bq-select.tsx | 14 ++++---- .../src/components/select/scss/bq-select.scss | 34 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/bee-q/src/components/select/bq-select.tsx b/packages/bee-q/src/components/select/bq-select.tsx index cc4aa92d3..bca222de9 100644 --- a/packages/bee-q/src/components/select/bq-select.tsx +++ b/packages/bee-q/src/components/select/bq-select.tsx @@ -274,7 +274,7 @@ export class BqSelect { {/* Label */}