Skip to content

Commit

Permalink
Improve PeopleSelect styles (#634)
Browse files Browse the repository at this point in the history
* Add offset, improve styles

* Improve dropdown design

* Style tweak

* WIP "Loading"

* Improve loading states

* Cleanup

* Update people-select-test.ts
  • Loading branch information
jeffdaley authored Mar 6, 2024
1 parent c5acf53 commit 821f352
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 28 deletions.
16 changes: 10 additions & 6 deletions web/app/components/inputs/people-select.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@
@onKeydown={{@onKeydown}}
@disabled={{@disabled}}
@selectedItemComponent={{component "multiselect/user-email-image-chip"}}
@calculatePosition={{this.calculatePosition}}
@loadingMessage="Loading..."
@eventType="click"
{{on "click" this.onClick}}
...attributes
as |value|
>
<div class="flex items-center gap-2 overflow-hidden py-1">
<Person::Avatar @email={{value}} @size="medium" class="shrink-0" />
<div class="w-full">
<div class="text-body-100 leading-tight">
<Person::Avatar @email={{value}} />
<div class="w-full min-w-0">
<div class="truncate text-body-200 leading-tight">
{{get-model-attr "person.name" value}}
</div>
<div class="text-body-100 leading-tight opacity-70">
{{get-model-attr "person.email" value}}
<span class="ml-0.5 text-body-100 text-color-foreground-disabled">
{{get-model-attr "person.email" value}}
</span>
</div>
</div>
</div>
Expand Down
100 changes: 98 additions & 2 deletions web/app/components/inputs/people-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,37 @@ import FetchService from "hermes/services/fetch";
import Ember from "ember";
import StoreService from "hermes/services/store";
import PersonModel from "hermes/models/person";
import { Select } from "ember-power-select/components/power-select";
import { next } from "@ember/runloop";
import calculatePosition from "ember-basic-dropdown/utils/calculate-position";
import { assert } from "@ember/debug";

export interface GoogleUser {
emailAddresses: { value: string }[];
names: { displayName: string; givenName: string }[];
photos: { url: string }[];
}

enum ComputedVerticalPosition {
Above = "above",
Below = "below",
}

enum ComputedHorizontalPosition {
Left = "left",
Right = "right",
}

interface CalculatePositionOptions {
horizontalPosition: ComputedHorizontalPosition;
verticalPosition: ComputedVerticalPosition;
matchTriggerWidth: boolean;
previousHorizontalPosition?: ComputedHorizontalPosition;
previousVerticalPosition?: ComputedVerticalPosition;
renderInPlace: boolean;
dropdown: any;
}

interface InputsPeopleSelectComponentSignature {
Element: HTMLDivElement;
Args: {
Expand All @@ -40,26 +64,98 @@ export default class InputsPeopleSelectComponent extends Component<InputsPeopleS
*/
@tracked protected people: string[] = [];

/**
* The action to run when the PowerSelect input is clicked.
* Prevents the dropdown from opening when the input is empty.
*/
@action protected onClick(e: MouseEvent) {
const target = e.target as HTMLElement;
const input = target.closest(".ember-power-select-trigger") as HTMLElement;

if (input) {
const value = input.querySelector("input")?.value;
if (value === "") {
e.stopImmediatePropagation();
}
}
}

/**
* An action occurring on every keystroke.
* Handles cases where the user clears the input,
* since `onChange` is not called in that case.
* See: https://ember-power-select.com/docs/custom-search-action
*/
@action onInput(inputValue: string) {
@action protected onInput(inputValue: string, select: Select) {
if (inputValue === "") {
this.people = [];

/**
* Stop the redundant "type to search" message
* from appearing when the last character is deleted.
*/
next(() => {
select.actions.close();
});
}
}

/**
* The action taken when focus leaves the component.
* Clears the people list and calls `this.args.onBlur` if it exists.
*/
@action onClose() {
@action protected onClose() {
this.people = [];
}

/**
* Custom position-calculating function for the dropdown.
* Ensures the dropdown is at least 320px wide, instead of 100% of the trigger.
* Improves dropdown appearance when the trigger is small, e.g., in the sidebar.
*/
@action protected calculatePosition(
trigger: HTMLElement,
content: HTMLElement,
destination: HTMLElement,
options: CalculatePositionOptions,
) {
const position = calculatePosition(trigger, content, destination, options);

const extraOffsetLeft = 4;
const extraOffsetBelow = 2;
const extraOffsetAbove = extraOffsetBelow + 2;

const { verticalPosition, horizontalPosition } = position;

let { top, left, width } = position.style;

assert("top must be a number", typeof top === "number");
assert("left must be a number", typeof left === "number");
assert("width must be a number", typeof width === "number");

switch (verticalPosition) {
case ComputedVerticalPosition.Above:
top -= extraOffsetAbove;
break;
case ComputedVerticalPosition.Below:
top += extraOffsetBelow;
break;
}

switch (horizontalPosition) {
case ComputedHorizontalPosition.Left:
left -= extraOffsetLeft;
break;
}

position.style.top = top;
position.style.left = left;
position.style.width = width + extraOffsetLeft * 2;
position.style["min-width"] = `320px`;

return position;
}

/**
* A task that queries the server for people matching the given query.
* Used as the `search` action for the `ember-power-select` component.
Expand Down
12 changes: 8 additions & 4 deletions web/app/styles/components/multiselect.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,26 @@
}

.ember-power-select-multiple-options {
@apply flex flex-wrap h-full mr-6;
@apply mr-6 flex h-full flex-wrap gap-2.5;
}

.ember-power-select-trigger-multiple-input {
min-width: 80px;
@apply min-w-[80px] px-[3px] text-body-200 placeholder:text-color-foreground-disabled;

&::-webkit-search-cancel-button {
display: none;
}
}

.ember-power-select-multiple-option {
@apply flex items-center px-0 m-0.5 max-w-[100%] relative pr-7 pl-1.5 py-1 border-0;
@apply relative inline-flex max-w-full items-center border-0 text-color-foreground-strong;

// Compensate for the chip padding by setting negative margins.
// Allows content to maintain its position in both the edit and read-only states.
@apply -my-1 -ml-[3px] -mr-1 py-1 px-0 pl-[3px] pr-7;

.ember-power-select-multiple-remove-btn {
@apply absolute right-1 grid place-items-center h-5 w-5 shrink-0 z-10 rounded-sm text-color-foreground-primary pb-px;
@apply absolute right-1 z-10 grid h-5 w-5 shrink-0 place-items-center rounded-sm pb-px text-color-foreground-primary;

&:hover {
@apply bg-neutral-200;
Expand Down
2 changes: 1 addition & 1 deletion web/app/styles/components/sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
}

.person-list {
@apply w-full space-y-2;
@apply grid w-full gap-2.5;
}

.sidebar-footer {
Expand Down
53 changes: 41 additions & 12 deletions web/app/styles/ember-power-select-theme.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@import "ember-basic-dropdown";
// https://github.com/cibernox/ember-power-select/blob/master/ember-power-select/scss/variables.scss

// ember-power-select
$ember-power-select-background-color: var(
--token-form-control-base-surface-color-default
);
Expand All @@ -10,8 +9,9 @@ $ember-power-select-disabled-background-color: var(
$ember-power-select-multiple-selection-background-color: var(
--token-color-palette-neutral-100
);
$ember-power-select-highlighted-color: inherit;
$ember-power-select-highlighted-background: var(
--token-color-foreground-action
--token-color-surface-interactive-hover
);
$ember-power-select-border-color: var(
--token-form-control-base-border-color-default
Expand All @@ -21,24 +21,53 @@ $ember-power-select-default-border: var(--token-form-control-border-width) solid
$ember-power-select-default-border-radius: var(
--token-form-control-border-radius
);

$ember-power-select-opened-border-radius: var(
--token-form-control-border-radius
);

$ember-power-select-focus-box-shadow: var(--token-elevation-low-box-shadow);
$ember-power-select-dropdown-box-shadow: var(--token-elevation-low-box-shadow);
$ember-power-select-option-padding: 7px; // Can't use --token-form-control-padding here.
$ember-power-select-option-padding: 8px;
$ember-power-select-focus-outline: 3px solid
var(--token-color-focus-action-external);
$ember-power-select-trigger-ltr-padding: var(--token-form-control-padding);
$ember-power-select-trigger-rtl-padding: var(--token-form-control-padding);
$ember-power-select-multiple-option-padding: 0 7px;
$ember-power-select-trigger-ltr-padding: 7px 5px;
$ember-power-select-trigger-rtl-padding: 7px 5px;
$ember-power-select-multiple-option-padding: 0;
$ember-power-select-multiple-option-line-height: 1.3;

.ember-basic-dropdown-content {
.hds-dropdown-list-item--variant-interactive {
@apply flex items-center px-3;
@apply min-h-[34px];

&:hover {
@apply bg-color-surface-interactive-hover;
}
.hds-dropdown-list-item--variant-interactive {
@apply flex items-center px-2;
}
}

@import "ember-power-select";

.ember-basic-dropdown-content {
border: none !important;
@apply hds-surface-high mt-px;
}

.ember-power-select-options[role="listbox"] {
@apply max-h-[200px];
}

.ember-power-select-option {
@apply py-px;

&:first-child {
@apply mt-[3px];
}

&:last-child {
@apply mb-[3px];
}
}

.ember-power-select-option--loading-message,
.ember-power-select-option--no-matches-message {
@apply h-7 px-3.5 text-color-foreground-faint;
}
5 changes: 2 additions & 3 deletions web/tests/integration/components/inputs/people-select-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ module("Integration | Component | inputs/people-select", function (hooks) {

assert
.dom(".ember-power-select-option")
.exists({ count: 1 })
.hasText("Type to search");
.doesNotExist('"Type to search" message is hidden');

await fillIn(".ember-power-select-trigger-multiple-input", "u");

Expand Down Expand Up @@ -115,7 +114,7 @@ module("Integration | Component | inputs/people-select", function (hooks) {

assert
.dom(".ember-power-select-option--loading-message")
.hasText("Loading options...");
.hasText("Loading...");

await fillInPromise;

Expand Down

0 comments on commit 821f352

Please sign in to comment.