Skip to content

Commit

Permalink
feat: create sort button element
Browse files Browse the repository at this point in the history
  • Loading branch information
eyevana committed Sep 18, 2023
1 parent 1c06479 commit 26e6898
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 129 deletions.
43 changes: 43 additions & 0 deletions elements/rh-table/demo/rh-table.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,49 @@
<script type="module" src="rh-table.js"></script>

<section>
<rh-table disclaimer="Disclaimer">
<table>
<caption>Sortable</caption>
<col/>
<col/>
<col/>
<thead>
<tr>
<th scope="col" data-label="Date">
Date
</th>
<th scope="col" data-label="Event">
Event
<rh-sort-button column="event"></rh-sort-button>
</th>
<th scope="col" data-label="Venue">
Venue
<rh-sort-button column="venue"></rh-sort-button>
</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Date">12 February</td>
<td data-label="Event">
<a href="">Waltz with Strauss</a>
</td>
<td data-label="Venue">Main Hall</td>
</tr>
<tr>
<td data-label="Date">24 March</td>
<td data-label="Event">The Obelisks</td>
<td data-label="Venue">West Wing</td>
</tr>
<tr>
<td data-label="Date">14 April</td>
<td data-label="Event">The What</td>
<td data-label="Venue">Main Hall</td>
</tr>
</tbody>
</table>
</rh-table>

<rh-table disclaimer="Disclaimer">
<table>
<caption>Title, headers, and disclaimer</caption>
Expand Down
25 changes: 25 additions & 0 deletions elements/rh-table/rh-sort-button.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#sort-button {
background-color: transparent;
border: 0;
}

#sort-button:after {
content: "";
position: absolute;
inset: 0;
cursor: pointer;
}

#sort-button #sort-indicator {
color: var(--rh-color-gray-30, #a3a3a3);
}

.visually-hidden {
position: fixed;
top: 0;
left: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
74 changes: 74 additions & 0 deletions elements/rh-table/rh-sort-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { LitElement, html, svg } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { classMap } from 'lit/directives/class-map.js';
import { property } from 'lit/decorators/property.js';
import styles from './rh-sort-button.css';

const DIRECTIONS = { asc: 'desc', desc: 'asc' } as const;

export class RequestSortEvent extends Event {
constructor(
public direction: 'asc' | 'desc',
) {
super('request-sort', {
bubbles: true,
cancelable: true,
});
}
}

// TODO need finalized icons from designers
const paths = new Map(Object.entries({
asc: 'M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z',
desc: 'M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z',
sort: 'M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z',
}));

/**
* Table sort button
* @slot - Place element content here
*/
@customElement('rh-sort-button')
export class RhSortButton extends LitElement {
static readonly styles = [styles];

@property({ type: Boolean, reflect: true }) selected?: boolean = false;

@property({
reflect: true,
attribute: 'sort-direction',
}) sortDirection?: 'asc' | 'desc';

@property() column?: string;

render() {
const selected = !!this.selected;
return html`
<button id="sort-button" class="sortable ${classMap({ selected })}" part="sort-button" @click="${this.sort}">
<span class="visually-hidden">${!this.sortDirection ? '' : `(sort${!this.column ? '' : ` by ${this.column}`} in ${this.sortDirection === 'asc' ? 'ascending' : 'descending'} order)`}</span>
<span id="sort-indicator">
<svg fill="currentColor"
height="1em"
width="1em"
viewBox="0 0 320 512"
aria-hidden="true"
role="img"
style="vertical-align: -0.125em;">
${svg`<path d="${paths.get(this.sortDirection ?? 'sort')}"></path>`}
</svg>
</span>
</button>
`;
}

sort() {
const next = DIRECTIONS[this.sortDirection ?? 'asc'];
this.dispatchEvent(new RequestSortEvent(next));
}
}

declare global {
interface HTMLElementTagNameMap {
'rh-sort-button': RhSortButton;
}
}
52 changes: 4 additions & 48 deletions elements/rh-table/rh-table-lightdom.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
border-collapse: collapse;
}

:is(rh-table) th {
position: relative;
}

:is(rh-table) tr {
border-bottom: 1px solid #d2d2d2;
}
Expand Down Expand Up @@ -45,54 +49,6 @@
text-align: center;
}

/* Sortable columns */
:is(rh-table) th[sortable] {
padding: 0;
}

:is(rh-table) th[sortable] .visually-hidden {
position: fixed;
top: 0;
left: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

:is(rh-table) :not(th[sort-direction]) .sort-indicator {
color: var(--rh-color-gray-30, #a3a3a3);
}

:is(rh-table) th[sort-direction] .sort-indicator {
color: inherit;
transform: rotate(180deg);
}

:is(rh-table) :not(th[sort-direction]) .visually-hidden {
display: none;
}

:is(rh-table) th[sort-direction] .visually-hidden {
display: unset;
}

:is(rh-table) th[sortable] .sort-button {
cursor: pointer;
font-family: inherit;
font-weight: inherit;
font-size: inherit;
display: flex;
align-items: center;
justify-content: center;
gap: var(--rh-space-sm, 6px);
background-color: transparent;
border: 0;
width: 100%;
padding: var(--rh-space-lg, 16px);
}


/* Desktop */
:is(rh-table) :is(th, td) {
padding: var(--rh-space-xl, 24px) var(--rh-space-lg, 16px);
Expand Down
101 changes: 20 additions & 81 deletions elements/rh-table/rh-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,11 @@ import styles from './rh-table.css';
import { classMap } from 'lit/directives/class-map.js';
import { ScreenSizeController } from '../../lib/ScreenSizeController.js';
import { property } from 'lit/decorators/property.js';


// TODO: replace with rh-icon
const ICONS = {
asc: {
viewBox: '0 0 320 512',
path: 'M177 159.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 255.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 329.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1z',
},
desc: {
viewBox: '0 0 320 512',
path: 'M143 352.3L7 216.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.2 9.4-24.4 9.4-33.8 0z',
},
get(name: 'asc' | 'desc') {
const { viewBox, path } = ICONS[name];
return svg`
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
height="1em"
width="1em"
style="vertical-align:-0.125em"
viewBox="${viewBox}">
<path d="${path}"/>
</svg>`;
},
};
import { RequestSortEvent, RhSortButton } from './rh-sort-button.js';
export * from './rh-sort-button.js';

/**
* Table
* Table sort button
* @slot - Place element content here
*/
@customElement('rh-table')
Expand Down Expand Up @@ -68,7 +43,8 @@ export class RhTable extends LitElement {
class=${classMap({ mobile })}
@pointerleave=${this.#onPointerleave}
@pointerover=${this.#onPointerover}>
<slot @slotchange="${this.#onSlotchange}"></slot>
<slot @slotchange="${this.#onSlotchange}"
@request-sort="${this.#onRequestSort}"></slot>
<slot name="disclaimer">
${!this.disclaimer ? nothing : html`<small id="disclaimer" part="disclaimer">${this.disclaimer}</small>`}
</slot>
Expand Down Expand Up @@ -115,66 +91,29 @@ export class RhTable extends LitElement {
});
}

#sortButtonTemplate(children: NodeListOf<ChildNode>) {
return html`
<button class="sort-button"
@click="${this.#onSort.bind(this)}">
${[...children]}
<span class="visually-hidden"></span>
<span class="sort-indicator"></span>
</button>`;
}

#onSlotchange() {
if (!this.#sortableHeaders) {
return;
}

// Add button to sortable headers
for (const header of this.#sortableHeaders) {
render(this.#sortButtonTemplate(header.childNodes), header);
}
//
}

#onSort(event: Event) {
// update selected
const selected = event.currentTarget as HTMLButtonElement;
const selectedParent = selected.closest('th');

const sorted = (selectedParent?.getAttribute('sort-direction') ?? 'asc');
const direction = sorted === 'asc' ? 'desc' : 'asc';

selectedParent?.setAttribute('aria-sort', `${direction}ending`);
selectedParent?.setAttribute('sort-direction', direction);

const iconContainer = selected.querySelector('.sort-indicator') as HTMLSpanElement;
render(ICONS.get(direction), iconContainer);

const srContainer = selected.querySelector('.visually-hidden') as HTMLSpanElement;
render(`(sorted ${direction}ending)`, srContainer);

if (!this.#sortableHeaders) {
this.#logger.warn('No sortable headers found');
return;
}

// clean up previously sorted headers
for (const header of this.#sortableHeaders) {
if (header !== selectedParent) {
header.removeAttribute('sort-direction');
header.removeAttribute('aria-sort');
#onRequestSort(event: Event) {
if (event instanceof RequestSortEvent) {
for (const button of this.querySelectorAll<RhSortButton>('rh-sort-button')) {
button.selected = button === event.target;
if (button !== event.target) {
button.removeAttribute('sort-direction');
}
}
if (!event.defaultPrevented && event.target instanceof RhSortButton) {
event.target.sortDirection = event.direction;
this.#performSort(event.target, event.direction);
}
}

// only sort if it hasn't been handled already
if (!event.defaultPrevented && selectedParent instanceof HTMLTableCellElement) {
this.#performSort(selectedParent, direction);
}
}

// TODO should we move the remaining methods into a controller?
#performSort(header: HTMLTableCellElement, direction: 'asc' | 'desc') {
const children = header.parentElement?.children;
#performSort(button: RhSortButton, direction: 'asc' | 'desc') {
const header = button.closest('th');
const children = header?.parentElement?.children;
if (children) {
const columnIndexToSort = [...children].indexOf(header);

Expand Down

0 comments on commit 26e6898

Please sign in to comment.