Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Slider): allow to enable/show a Tooltip over the Slider thumb(s) #999

Merged
merged 11 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/beeq/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,10 @@ export namespace Components {
* If `true` the slider is disabled.
*/
"disabled"?: boolean;
/**
* If `true`, a tooltip will be shown displaying the progress value
*/
"enableTooltip": boolean;
/**
* If `true` it will show the value label on a side of the slider track area
*/
Expand All @@ -939,6 +943,10 @@ export namespace Components {
* A number representing the step of the slider. ⚠️ Please notice that the value (or list of values if the slider type is `range`) will be rounded to the nearest multiple of `step`.
*/
"step": number;
/**
* If `true`, a tooltip will always display the progress value. It relies on enableTooltip and if enableTooltip is false, tooltipAlwaysVisible cannot be true.
*/
"tooltipAlwaysVisible": boolean;
/**
* It defines the type of slider to display
*/
Expand Down Expand Up @@ -3104,6 +3112,10 @@ declare namespace LocalJSX {
* If `true` the slider is disabled.
*/
"disabled"?: boolean;
/**
* If `true`, a tooltip will be shown displaying the progress value
*/
"enableTooltip"?: boolean;
/**
* If `true` it will show the value label on a side of the slider track area
*/
Expand Down Expand Up @@ -3136,6 +3148,10 @@ declare namespace LocalJSX {
* A number representing the step of the slider. ⚠️ Please notice that the value (or list of values if the slider type is `range`) will be rounded to the nearest multiple of `step`.
*/
"step"?: number;
/**
* If `true`, a tooltip will always display the progress value. It relies on enableTooltip and if enableTooltip is false, tooltipAlwaysVisible cannot be true.
*/
"tooltipAlwaysVisible"?: boolean;
/**
* It defines the type of slider to display
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const meta: Meta = {
'debounce-time': { control: 'number' },
disabled: { control: 'boolean' },
'enable-value-indicator': { control: 'boolean' },
'enable-tooltip': { control: 'boolean' },
'tooltip-always-visible': { control: 'boolean' },
gap: { control: 'number' },
max: { control: 'number' },
min: { control: 'number' },
Expand All @@ -32,6 +34,8 @@ const meta: Meta = {
'debounce-time': 0,
disabled: false,
'enable-value-indicator': false,
'enable-tooltip': false,
'tooltip-always-visible': false,
gap: 0,
max: 100,
min: 0,
Expand All @@ -50,6 +54,8 @@ const Template = (args: Args) => html`
debounce-time=${ifDefined(args['debounce-time'])}
?disabled=${args.disabled}
?enable-value-indicator=${args['enable-value-indicator']}
?enable-tooltip=${args['enable-tooltip']}
?tooltip-always-visible=${args['tooltip-always-visible']}
gap=${ifDefined(args.gap)}
max=${ifDefined(args.max)}
min=${ifDefined(args.min)}
Expand Down Expand Up @@ -134,3 +140,16 @@ export const DecimalValues: Story = {
value: [0.3, 0.7],
},
};

export const WithTooltip: Story = {
render: Template,
args: {
'enable-tooltip': true,
gap: 10,
max: 100,
min: 0,
step: 1,
type: 'range',
value: [30, 70],
},
};
103 changes: 99 additions & 4 deletions packages/beeq/src/components/slider/bq-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export class BqSlider {

private inputMinElem: HTMLInputElement;
private inputMaxElem: HTMLInputElement;
private minTooltipElem: HTMLBqTooltipElement;
private maxTooltipElem: HTMLBqTooltipElement;
private progressElem: HTMLSpanElement;
private trackElem: HTMLSpanElement;
private debounceBqChange: TDebounce<void>;

// Reference to host HTML element
Expand All @@ -45,6 +48,10 @@ export class BqSlider {
@State() minValue: number;
/** The `maxValue` state is only used when the slider type is `range`. */
@State() maxValue: number;
/** It hold the left position of the Thumb for the value or the minimum value (if the slider type is `range`) */
@State() minThumbPosition: number;
/** It hold the left position of the Thumb for the maximum value (if the slider type is `range`) */
@State() maxThumbPosition: number;

// Public Property API
// ========================
Expand Down Expand Up @@ -83,6 +90,15 @@ export class BqSlider {
*/
@Prop({ reflect: true, mutable: true }) value: TSliderValue;

/** If `true`, a tooltip will be shown displaying the progress value */
@Prop({ reflect: true }) enableTooltip: boolean = false;

/**
* If `true`, a tooltip will always display the progress value.
* It relies on enableTooltip and if enableTooltip is false, tooltipAlwaysVisible cannot be true.
*/
@Prop({ reflect: true }) tooltipAlwaysVisible: boolean = false;

// Prop lifecycle events
// =======================

Expand Down Expand Up @@ -132,13 +148,11 @@ export class BqSlider {
}

componentDidLoad() {
this.updateProgressTrack();
this.syncInputsValue();
this.runUpdates();
}

componentDidUpdate() {
this.updateProgressTrack();
this.syncInputsValue();
this.runUpdates();
}

// Listeners
Expand All @@ -156,6 +170,12 @@ export class BqSlider {
// These methods cannot be called from the host element.
// =======================================================

private runUpdates = () => {
this.updateProgressTrack();
this.syncInputsValue();
this.setThumbPosition();
};

private calculateMinValue = (value: TSliderValue) => {
const isMaxValue = (this.maxValue ?? value[1]) === this.max;
const isGapExceeded = value[0] + this.gap > this.max;
Expand All @@ -173,6 +193,13 @@ export class BqSlider {
this.maxValue = isRangeType ? this.calculateMaxValue(value, this.minValue) : this.minValue;
};

private setThumbPosition = () => {
if (!this.enableTooltip) return;

// Destructure the returned object from this.thumbPosition() and assign the properties to this.minThumbPosition and this.maxThumbPosition
({ minThumbPosition: this.minThumbPosition, maxThumbPosition: this.maxThumbPosition } = this.thumbPosition());
};

private syncInputsValue = () => {
this.inputMinElem?.setAttribute('value', this.minValue.toString());
this.inputMaxElem?.setAttribute('value', this.maxValue.toString());
Expand Down Expand Up @@ -217,6 +244,26 @@ export class BqSlider {
this.progressElem.style.width = `${width}%`;
};

private calculateThumbPosition = (value: number): number => {
if (!this.progressElem) return;

// Get the width of the track area and the size of the input range thumb
const trackAreaWidth = this.trackElem.getBoundingClientRect().width;
// We need to also add 4px to the thumb size,
// this is because the thumb is 2px border (`border-2`)
const inputThumbSize = parseInt(getComputedStyle(this.el).getPropertyValue('--bq-slider--thumb-size'), 10) + 4;
const totalWidth = trackAreaWidth - inputThumbSize;

return ((value - this.min) / (this.max - this.min)) * totalWidth + inputThumbSize / 2;
};

private thumbPosition = (): { minThumbPosition: number; maxThumbPosition?: number } => {
const minThumbPosition = this.calculateThumbPosition(this.minValue);
const maxThumbPosition = this.isRangeType ? this.calculateThumbPosition(this.maxValue) : undefined;

return { minThumbPosition, maxThumbPosition };
};

private emitBqChange = () => {
this.debounceBqChange?.cancel();

Expand All @@ -234,6 +281,22 @@ export class BqSlider {
this.bqFocus.emit(this.el);
};

private handleMouseDown = (event: MouseEvent) => {
this.handleTooltipVisibility(event, 'remove');
};

private handleMouseUp = (event: MouseEvent) => {
this.handleTooltipVisibility(event, 'add');
};

private handleTooltipVisibility = (event: MouseEvent, action: 'add' | 'remove') => {
if (!this.enableTooltip || this.tooltipAlwaysVisible) return;

const target = event.target as HTMLElement;
const tooltipElem = target === this.inputMinElem ? this.minTooltipElem : this.maxTooltipElem;
tooltipElem.classList[action]('hidden');
};

private get decimalCount(): number {
// Return the length of the decimal part of the step value.
return (this.step % 1).toFixed(10).split('.')[1].replace(/0+$/, '').length;
Expand All @@ -243,6 +306,10 @@ export class BqSlider {
return this.type === 'range';
}

private get isTooltipAlwaysVisible(): boolean {
return this.tooltipAlwaysVisible && this.enableTooltip;
}

private renderLabel = (value: number, position: 'start' | 'end', css?: string) => {
return (
<span
Expand Down Expand Up @@ -275,12 +342,32 @@ export class BqSlider {
onInput={(ev) => this.handleInputChange(type, ev)}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
value={value}
part={`input-${type}`}
/>
);
};

private renderTooltip = (
value: number,
thumbPosition: number,
refCallback: (elem: HTMLBqTooltipElement) => void,
): HTMLBqTooltipElement => (
<bq-tooltip
class={{ absolute: true, hidden: !this.isTooltipAlwaysVisible }}
exportparts="base,trigger,panel"
alwaysVisible={true}
distance={this.enableValueIndicator ? 6 : 16}
style={{ left: `${thumbPosition}px`, fontVariant: 'tabular-nums' }}
ref={refCallback}
>
<div class="absolute size-1" slot="trigger" />
{value.toFixed(this.decimalCount)}
</bq-tooltip>
);

// render() function
// Always the last one in the class.
// ===================================
Expand All @@ -299,6 +386,7 @@ export class BqSlider {
{/* TRACK AREA */}
<span
class="absolute top-1/2 h-1 w-full -translate-y-1/2 rounded-xs bg-[--bq-slider--trackarea-color]"
ref={(elem) => (this.trackElem = elem)}
part="track-area"
/>
{/* PROGRESS AREA */}
Expand All @@ -307,8 +395,15 @@ export class BqSlider {
ref={(elem) => (this.progressElem = elem)}
part="progress-area"
/>
{/* TOOLTIP on top of the value or min value (if the slider type is `range`) */}
{this.enableTooltip &&
this.renderTooltip(this.minValue, this.minThumbPosition, (elem) => (this.minTooltipElem = elem))}
{/* INPUT (Min), used on single type */}
{this.renderInput('min', this.minValue, (input) => (this.inputMinElem = input))}
{/* TOOLTIP on top of the max value (if the slider type is `range`) */}
{this.enableTooltip &&
this.isRangeType &&
this.renderTooltip(this.maxValue, this.maxThumbPosition, (elem) => (this.maxTooltipElem = elem))}
{/* INPUT (Max) */}
{this.isRangeType && this.renderInput('max', this.maxValue, (input) => (this.inputMaxElem = input))}
</div>
Expand Down
15 changes: 15 additions & 0 deletions packages/beeq/src/components/slider/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
| ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ----------- |
| `debounceTime` | `debounce-time` | The amount of time, in milliseconds, to wait to trigger the `bqChange` event after each value change. | `number` | `0` |
| `disabled` | `disabled` | If `true` the slider is disabled. | `boolean` | `false` |
| `enableTooltip` | `enable-tooltip` | If `true`, a tooltip will be shown displaying the progress value | `boolean` | `false` |
| `enableValueIndicator` | `enable-value-indicator` | If `true` it will show the value label on a side of the slider track area | `boolean` | `false` |
| `gap` | `gap` | A number representing the amount to remain between the minimum and maximum values (only for range type). | `number` | `0` |
| `max` | `max` | A number representing the max value of the slider. | `number` | `100` |
| `min` | `min` | A number representing the min value of the slider. | `number` | `0` |
| `step` | `step` | A number representing the step of the slider. ⚠️ Please notice that the value (or list of values if the slider type is `range`) will be rounded to the nearest multiple of `step`. | `number` | `1` |
| `tooltipAlwaysVisible` | `tooltip-always-visible` | If `true`, a tooltip will always display the progress value. It relies on enableTooltip and if enableTooltip is false, tooltipAlwaysVisible cannot be true. | `boolean` | `false` |
| `type` | `type` | It defines the type of slider to display | `"range" \| "single"` | `'single'` |
| `value` | `value` | The value of the slider. - If the slider type is `single`, the value is a number. - If the slider type is `range`, the value is an array of two numbers (the first number represents the `min` value and the second number represents the `max` value). | `[number, number] \| number \| string` | `undefined` |

Expand Down Expand Up @@ -43,6 +45,19 @@
| `"track-area"` | The track area of the slider. |


## Dependencies

### Depends on

- [bq-tooltip](../tooltip)

### Graph
```mermaid
graph TD;
bq-slider --> bq-tooltip
style bq-slider fill:#f9f,stroke:#333,stroke-width:4px
```

----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
2 changes: 2 additions & 0 deletions packages/beeq/src/components/tooltip/bq-tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export class BqTooltip {
handleFloatingUIOptionsChange() {
this.floatingUI.init({
placement: this.placement,
distance: this.distance,
sameWidth: this.sameWidth,
strategy: 'fixed',
});
}
Expand Down
2 changes: 2 additions & 0 deletions packages/beeq/src/components/tooltip/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ Type: `Promise<void>`

- [bq-progress](../progress)
- [bq-side-menu-item](../side-menu-item)
- [bq-slider](../slider)

### Graph
```mermaid
graph TD;
bq-progress --> bq-tooltip
bq-side-menu-item --> bq-tooltip
bq-slider --> bq-tooltip
style bq-tooltip fill:#f9f,stroke:#333,stroke-width:4px
```

Expand Down