Skip to content

Commit

Permalink
feat(ld-select): max rows
Browse files Browse the repository at this point in the history
  • Loading branch information
borisdiakur committed Jul 20, 2021
1 parent 14df7a5 commit a783c88
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 29 deletions.
32 changes: 29 additions & 3 deletions src/liquid/components/ld-select/ld-select.css
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@
}

.ld-select:where(:not(.ld-select--ghost)) {
&:where(:not(.ld-select--expanded)) {
&:where(.ld-select--detached),
&:where(:not(.ld-select--detached):not(.ld-select--expanded)) {
:where(select),
:where(.ld-select__btn-trigger) {
box-shadow: inset 0 0 0 0.1rem var(--ld-col-rblck1);
Expand All @@ -150,9 +151,10 @@
&:where(:not(.ld-select--invalid)) {
select,
.ld-select__btn-trigger {
&:where(:not(:disabled):not([aria-disabled='true'])) {
&:where(.ld-select__btn-trigger--detached:not(:disabled):not([aria-disabled='true'])),
&:where(:not(:disabled):not([aria-disabled='true']):not(.ld-select__btn-trigger--detached):not([aria-expanded='true'])) {
@media (hover: hover) {
&:hover:not(:focus) {
&:hover:not(:focus:focus-visible) {
box-shadow: inset 0 0 0 0.1rem var(--ld-col-rblck2);
}
}
Expand Down Expand Up @@ -265,6 +267,27 @@
width: 100%;
}

.ld-select__selection-list-item--overflowing {
display: none;
}

.ld-select__selection-list-more {
order: 2147483647; /* Highest possible */
height: 100%;
display: inline-flex;
align-items: center;
font: var(--ld-typo-label-s);
color: var(--ld-col-wht);
font-weight: 700;
padding: var(--ld-sp-4) var(--ld-sp-6);
border-radius: var(--ld-br-m);
margin-right: var(--ld-sp-4);

:where(.ld-select__btn-trigger[aria-disabled='true']) & {
background-color: var(--ld-col-rblck1);
}
}

.ld-select__btn-clear-single,
.ld-select__btn-clear {
border: 0;
Expand Down Expand Up @@ -385,6 +408,7 @@
}

:where(.ld-select:not([disabled]):not([aria-disabled='true'])) {
.ld-select__selection-list-more,
.ld-select__selection-label-bg {
background-color: var(--ld-thm-ocean-bg-primary);
}
Expand Down Expand Up @@ -496,6 +520,7 @@
}

:where(.ld-select:not([disabled]):not([aria-disabled='true'])) {
.ld-select__selection-list-more,
.ld-select__selection-label-bg {
background-color: var(--ld-col-rp-default);
}
Expand Down Expand Up @@ -603,6 +628,7 @@
}

:where(.ld-select:not([disabled]):not([aria-disabled='true'])) {
.ld-select__selection-list-more,
.ld-select__selection-label-bg {
background-color: var(--ld-thm-tea-bg-primary);
}
Expand Down
136 changes: 124 additions & 12 deletions src/liquid/components/ld-select/ld-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class LdSelect {

private selectRef!: HTMLElement
private triggerRef!: HTMLElement
private selectionListRef!: HTMLElement
private slotContainerRef!: HTMLElement
private internalOptionsContainerRef!: HTMLElement
private popperRef!: HTMLElement
Expand Down Expand Up @@ -76,8 +77,11 @@ export class LdSelect {
/** Size of the select trigger button. */
@Prop() size?: 'sm' | 'lg'

// TODO: implement compact mode
// Constrain the display
/**
* Constrains the height of the trigger button by replacing overflowing selection
* with a "+X more" indicator.
*/
@Prop() maxRows?: number

/** Attached as CSS class to the select popper element. */
@Prop() popperClass: string
Expand All @@ -103,6 +107,8 @@ export class LdSelect {

@State() internalOptionsHTML: string

@State() hasMore = false

@Watch('selected')
emitEvents(
newSelection: { value: string; text: string }[],
Expand Down Expand Up @@ -159,6 +165,93 @@ export class LdSelect {
*/
@Event({ bubbles: false }) blur: EventEmitter<string[]>

private updateTriggerMoreIndicator(refresh = false) {
if (!this.multiple) return

if (!this.maxRows || this.maxRows < 0) {
return
}

if (refresh) this.hasMore = false

window.requestAnimationFrame(() => {
if (!this.selectionListRef) return

const selectionListItems = Array.from(
this.selectionListRef.querySelectorAll(
'.ld-select__selection-list-item'
)
)

if (!this.hasMore) {
// reset
this.selectionListRef
.querySelector('.ld-select__selection-list-more')
?.remove()
selectionListItems.forEach((el) => {
el.classList.remove('ld-select__selection-list-item--overflowing')
})
}

// If overflowing, hide overflowing and show "+X" indicator
if (
this.selectionListRef.scrollHeight > this.selectionListRef.clientHeight
) {
let moreItem
if (!this.hasMore) {
moreItem = document.createElement('li')
moreItem.classList.add('ld-select__selection-list-more')
this.selectionListRef.prepend(moreItem)
} else {
moreItem = this.selectionListRef.querySelector(
'.ld-select__selection-list-more'
)
}
this.hasMore = true

const maxOffset = this.maxRows * 1.75 * 16

let overflowingTotal = 0
selectionListItems.forEach((el) => {
const overflowing = overflowingTotal
? true
: (el as HTMLElement).offsetTop >= maxOffset
el.classList[overflowing ? 'add' : 'remove'](
'ld-select__selection-list-item--overflowing'
)
if (overflowing) overflowingTotal++
})

const hideLastVisibleIfMoreIndicatorOverflowing = () => {
moreItem = this.selectionListRef.querySelector(
'.ld-select__selection-list-more'
)
moreItem.innerText = `+${overflowingTotal}`
if (moreItem.offsetTop < maxOffset) {
return
}

const notOverflowing = Array.from(
this.selectionListRef.querySelectorAll(
'.ld-select__selection-list-item:not(.ld-select__selection-list-item--overflowing)'
)
)
const [lastNotOverflowing] = notOverflowing.slice(-1)
lastNotOverflowing.classList.add(
'ld-select__selection-list-item--overflowing'
)
overflowingTotal++
moreItem.innerText = `+${overflowingTotal}`

window.requestAnimationFrame(() => {
hideLastVisibleIfMoreIndicatorOverflowing()
})
}
hideLastVisibleIfMoreIndicatorOverflowing()
}
})
}

private updatePopperWidth() {
this.popperRef.style.setProperty(
'width',
Expand Down Expand Up @@ -225,9 +318,11 @@ export class LdSelect {
const initialized = this.initialized
let children
if (!initialized) {
children = this.slotContainerRef.children
children = this.slotContainerRef.querySelectorAll('ld-option')
} else {
children = this.internalOptionsContainerRef.children
children = this.internalOptionsContainerRef.querySelectorAll(
'ld-option-internal'
)
}

if (!children.length) {
Expand Down Expand Up @@ -262,13 +357,13 @@ export class LdSelect {
}
this.selected = childrenArr
.filter((child) => {
const selectedAttr = child.getAttribute('selected')
return selectedAttr !== null && selectedAttr !== 'false'
return ((child as unknown) as LdOptionInternal).selected
})
.map((child) => ({
value: child.getAttribute('value'),
text: child.innerText,
}))
this.updateTriggerMoreIndicator(true)
})
}

Expand Down Expand Up @@ -320,6 +415,7 @@ export class LdSelect {
if (this.disabled || this.ariaDisabled) return // this is for a minor performance optimization only

this.updatePopperWidth()
this.updateTriggerMoreIndicator(true)
this.updatePopperShadowHeight()
}

Expand Down Expand Up @@ -675,14 +771,19 @@ export class LdSelect {
this.triggerRef.focus()
}

private handleClearSingleClick(ev: MouseEvent, option: LdOptionInternal) {
private handleClearSingleClick(ev: MouseEvent, optionValue) {
ev.preventDefault()
ev.stopImmediatePropagation()

if (this.disabled || this.ariaDisabled) return
;((option as unknown) as HTMLElement).dispatchEvent(
new KeyboardEvent('keydown', { key: ' ' })

this.selected = this.selected.filter(
(selection) => selection.value !== optionValue
)

this.popperRef
.querySelector(`ld-option-internal[value='${optionValue}']`)
.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }))
}

componentWillLoad() {
Expand Down Expand Up @@ -793,10 +894,21 @@ export class LdSelect {
<ul
class="ld-select__selection-list"
aria-label="Selected options"
ref={(el) => (this.selectionListRef = el as HTMLElement)}
style={{
maxHeight:
this.maxRows && this.maxRows > 0
? `${this.maxRows * 1.75}rem`
: undefined,
}}
>
{this.selected.map((selection, index) => {
return (
<li key={index} class="ld-select__selection-list-item">
<li
key={index}
class="ld-select__selection-list-item"
style={{ order: index + 1 + '' }}
>
<label class="ld-select__selection-label">
<span
class="ld-select__selection-label-text"
Expand All @@ -816,7 +928,7 @@ export class LdSelect {
this.handleClearSingleClick.call(
this,
ev,
selection
selection.value
)
}}
>
Expand Down Expand Up @@ -862,7 +974,7 @@ export class LdSelect {
fill="none"
viewBox="0 0 21 20"
>
<title>Clear selection</title>
<title>Clear all</title>
<path
fill="currentColor"
fill-rule="evenodd"
Expand Down
51 changes: 37 additions & 14 deletions src/liquid/components/ld-select/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ The feature set of the `ld-select` Web Component differs from its CSS Component
</ld-select>
{% endexample %}

#### Max rows

{% example %}
<ld-select placeholder="Pick some fruits" name="fruits" multiple max-rows="2">
<ld-option value="apple">Apple</ld-option>
<ld-option value="banana" selected>Banana</ld-option>
<ld-option value="strawberry" selected>Strawberry</ld-option>
<ld-option value="watermelon" disabled>Watermelon</ld-option>
<ld-option value="honeymelon">Honeymelon</ld-option>
<ld-option value="rasberry">Rasberry</ld-option>
<ld-option value="cherry" selected>Cherry</ld-option>
<ld-option value="blueberry">Blueberry</ld-option>
<ld-option value="peach" selected>Peach</ld-option>
<ld-option value="grape" selected>Grape</ld-option>
<ld-option value="fuyu persimmon" selected>Fuyu Persimmon</ld-option>
<ld-option value="monstera deliciosa">Monstera Deliciosa</ld-option>
<ld-option value="pear" selected>Pear</ld-option>
<ld-option value="pineapple" selected>Pineapple</ld-option>
<ld-option value="plum" selected>Plum</ld-option>
</ld-select>
{% endexample %}

### Disabled

{% example %}
Expand Down Expand Up @@ -694,20 +716,21 @@ The feature set of the `ld-select` Web Component differs from its CSS Component

## Properties

| Property | Attribute | Description | Type | Default |
| -------------------- | --------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------- |
| `disabled` | `disabled` | Disabled state of the component. | `boolean` | `false` |
| `invalid` | `invalid` | Set this property to `true` in order to mark the select visually as invalid. | `boolean` | `false` |
| `key` | `key` | for tracking the node's identity when working with lists | `string \| number` | `undefined` |
| `mode` | `mode` | Display mode. | `"detached" \| "ghost" \| "inline"` | `undefined` |
| `multiple` | `multiple` | Multiselect mode. | `boolean` | `false` |
| `name` | `name` | Used to specify the name of the control. | `string` | `undefined` |
| `placeholder` | `placeholder` | Used as trigger button label in multiselect mode and in single select mode if nothing is selected. | `string` | `undefined` |
| `popperClass` | `popper-class` | Attached as CSS class to the select popper element. | `string` | `undefined` |
| `preventDeselection` | `prevent-deselection` | Prevents a state with no options selected after initial selection in single select mode. | `boolean` | `false` |
| `ref` | `ref` | reference to component | `any` | `undefined` |
| `size` | `size` | Size of the select trigger button. | `"lg" \| "sm"` | `undefined` |
| `tetherOptions` | `tether-options` | Stringified tether options object to be merged with the default options. | `string` | `'{}'` |
| Property | Attribute | Description | Type | Default |
| -------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------- |
| `disabled` | `disabled` | Disabled state of the component. | `boolean` | `false` |
| `invalid` | `invalid` | Set this property to `true` in order to mark the select visually as invalid. | `boolean` | `false` |
| `key` | `key` | for tracking the node's identity when working with lists | `string \| number` | `undefined` |
| `maxRows` | `max-rows` | Constrains the height of the trigger button by replacing overflowing selection with a "+X more" indicator. | `number` | `undefined` |
| `mode` | `mode` | Display mode. | `"detached" \| "ghost" \| "inline"` | `undefined` |
| `multiple` | `multiple` | Multiselect mode. | `boolean` | `false` |
| `name` | `name` | Used to specify the name of the control. | `string` | `undefined` |
| `placeholder` | `placeholder` | Used as trigger button label in multiselect mode and in single select mode if nothing is selected. | `string` | `undefined` |
| `popperClass` | `popper-class` | Attached as CSS class to the select popper element. | `string` | `undefined` |
| `preventDeselection` | `prevent-deselection` | Prevents a state with no options selected after initial selection in single select mode. | `boolean` | `false` |
| `ref` | `ref` | reference to component | `any` | `undefined` |
| `size` | `size` | Size of the select trigger button. | `"lg" \| "sm"` | `undefined` |
| `tetherOptions` | `tether-options` | Stringified tether options object to be merged with the default options. | `string` | `'{}'` |


## Events
Expand Down

0 comments on commit a783c88

Please sign in to comment.