Skip to content

Commit

Permalink
feat(select): allow to focus option with key
Browse files Browse the repository at this point in the history
- Allow arrow `down` and `up` to focus select options
  • Loading branch information
Sukaato committed Jan 19, 2025
1 parent 5483fce commit 59887c9
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 37 deletions.
36 changes: 23 additions & 13 deletions packages/core/src/components/select/select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* @prop --max-height: Change max-height for dropdown content (apply only when screen width is more than 640px)
*/

@include join-item.item("details > summary");
@include join-item.item(".dropdown > .dropdown-trigger");

// Select
// ----------------------------------------------------------------
Expand All @@ -40,6 +40,18 @@ $minWidth: 12rem;
position: relative;
width: 100%;

&[open] {
svg {
transform: rotateX(180deg);
}

.dropdown-content {
pointer-events: all;
opacity: 1;
transform: rotateX(0);
}
}

&-trigger {
display: flex;
position: relative;
Expand All @@ -66,8 +78,12 @@ $minWidth: 12rem;
}

&-content {
pointer-events: none;
position: absolute;
z-index: 20;
opacity: 0;

display: block;

min-height: 2rem;
max-height: var(--max-height, 20rem);
Expand All @@ -82,9 +98,10 @@ $minWidth: 12rem;
background-color: var(--background);

color: var(--color);
animation-duration: 200ms;

animation-name: present;
perspective: 200px;
transform: rotateX(28deg);
transform-style: preserve-3d;
transition: 150ms ease-in 0s;
}

&-backdrop {
Expand All @@ -103,7 +120,7 @@ $minWidth: 12rem;
svg {
min-width: 24px;
margin-inline-start: auto;
transition: transform 150ms ease 0ms;
transition: transform 200ms ease 0ms;
}

pop-list {
Expand Down Expand Up @@ -139,13 +156,6 @@ $minWidth: 12rem;

}


:host(.select-expanded) {
svg {
transform: rotateX(180deg);
}
}

:host([bordered]) {
--border-color: #{theme.use_color("base.content", 0.2)};
}
Expand All @@ -161,7 +171,7 @@ $minWidth: 12rem;
gap: 0.5rem;
}

// Input Size
// Select Size
// ----------------------------------------------------------------

:host([size="xs"]) .dropdown-trigger {
Expand Down
82 changes: 59 additions & 23 deletions packages/core/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import {
Element,
Event,
type EventEmitter,
Fragment,
Host,
Method,
Prop,
State,
Watch,
h,
} from '@stencil/core';
import { ENTER, ESC, SPACE } from 'key-definitions';
import { ARROW_DOWN, ARROW_UP, ENTER, ESC, SPACE } from 'key-definitions';
import type { FormAssociatedInterface, Size } from 'src/interface';
import { componentConfig, config } from '#config';
import { ClickOutside } from '#utils/click-outside';
Expand Down Expand Up @@ -50,6 +49,8 @@ export class Select implements ComponentInterface, FormAssociatedInterface {
@Element() host!: HTMLElement;
@AttachInternals() internals: ElementInternals;

@State() options: HTMLPopSelectOptionElement[] = [];

@State() open = false;

@State() errorText: string;
Expand Down Expand Up @@ -382,6 +383,11 @@ export class Select implements ComponentInterface, FormAssociatedInterface {
};
};

private onSlotChange = () => {
const options = this.host.querySelectorAll('pop-select-option');
this.options = [...Array.from(options)];
};

private get values(): any[] {
const { value } = this;
if (value == null) return [];
Expand Down Expand Up @@ -415,10 +421,6 @@ export class Select implements ComponentInterface, FormAssociatedInterface {
return '';
}

private get options() {
return Array.from(this.host.querySelectorAll('pop-select-option'));
}

private getAriaLabel(text: string): string {
const { placeholder } = this;
const displayValue = text;
Expand Down Expand Up @@ -484,19 +486,36 @@ export class Select implements ComponentInterface, FormAssociatedInterface {
}}
value={this.value}
>
{this.options.map(option => (
<pop-item>
<pop-radio
checked={isOptionSelected(this.value, getOptionValue(option), this.compare)}
color={color === 'ghost' ? undefined : color}
disabled={option.disabled}
size={size}
value={getOptionValue(option)}
>
{option.textContent}
</pop-radio>
</pop-item>
))}
<pop-list>
{this.options.map((option, idx) => (
<pop-item>
<pop-radio
checked={isOptionSelected(this.value, getOptionValue(option), this.compare)}
color={color === 'ghost' ? undefined : color}
disabled={option.disabled}
onKeyUp={async ev => {
if (ev.key !== ARROW_DOWN.key && ev.key !== ARROW_UP.key) {
return;
}
ev.preventDefault();
const radios = this.dropdownRef?.querySelectorAll('pop-radio') ?? [];
if (radios.length === 0) {
return;
}

const previous = idx === 0 ? this.options.length - 1 : idx - 1;
const next = idx === this.options.length - 1 ? 0 : idx + 1;
const index = ev.key === ARROW_UP.key ? previous : next;
return Array.from(radios).at(index).setFocus();
}}
size={size}
value={getOptionValue(option)}
>
{option.textContent}
</pop-radio>
</pop-item>
))}
</pop-list>
</pop-radio-group>
);
}
Expand All @@ -505,13 +524,28 @@ export class Select implements ComponentInterface, FormAssociatedInterface {
const { color, size } = this;

return (
<Fragment>
{this.options.map(option => (
<pop-list>
{this.options.map((option, idx) => (
<pop-item>
<pop-checkbox
checked={isOptionSelected(this.value, getOptionValue(option), this.compare)}
color={color === 'ghost' ? undefined : color}
disabled={option.disabled ?? this.disabled}
onKeyUp={async ev => {
if (ev.key !== ARROW_DOWN.key && ev.key !== ARROW_UP.key) {
return;
}
ev.preventDefault();
const checkboxes = this.dropdownRef?.querySelectorAll('pop-checkbox') ?? [];
if (checkboxes.length === 0) {
return;
}

const previous = idx === 0 ? this.options.length - 1 : idx - 1;
const next = idx === this.options.length - 1 ? 0 : idx + 1;
const index = ev.key === ARROW_UP.key ? previous : next;
return Array.from(checkboxes).at(index).setFocus();
}}
onPopChange={async () => {
this.errorText = this.errorTextValue;
if (this.errorText) {
Expand All @@ -530,7 +564,7 @@ export class Select implements ComponentInterface, FormAssociatedInterface {
</pop-checkbox>
</pop-item>
))}
</Fragment>
</pop-list>
);
}

Expand Down Expand Up @@ -594,7 +628,7 @@ export class Select implements ComponentInterface, FormAssociatedInterface {
class="dropdown-content"
part="content"
>
<pop-list>{this.multiple ? this.renderCheckboxOptions() : this.renderRadioOptions()}</pop-list>
{this.multiple ? this.renderCheckboxOptions() : this.renderRadioOptions()}
</div>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: Element not focusable, handle by summary keyboard event */}
<div
Expand All @@ -614,6 +648,8 @@ export class Select implements ComponentInterface, FormAssociatedInterface {
</Show>
</div>
</Show>

<slot onSlotchange={this.onSlotChange} />
</Host>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/select/tests/form/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Select | Poppy-ui</title>
<title>Select Form | Poppy-ui</title>
<link rel="stylesheet" href="/dist/poppy/poppy.css">
<script type="module" src="/dist/poppy/poppy.esm.js"></script>
<script nomodule src="/dist/poppy/poppy.js"></script>
Expand Down
96 changes: 96 additions & 0 deletions packages/core/src/components/select/tests/join/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Select Join | Poppy-ui</title>
<link rel="stylesheet" href="/dist/poppy/poppy.css">
<script type="module" src="/dist/poppy/poppy.esm.js"></script>
<script nomodule src="/dist/poppy/poppy.js"></script>
<style>
main {
width: 100vw;
height: 100dvh;
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;

background-color: var(--base-300);
}

section {
display: flex;
flex-direction: column;
justify-content: center;
gap: .35rem;
}

div {
display: flex;
gap: .5rem;
}
</style>
</head>

<body>
<main>
<section>
<h2>Select - basic</h2>
<div>
<pop-select placeholder="Select a value">
<span slot="label">Label</span>
<pop-select-option value="1">Option 1</pop-select-option>
<pop-select-option value="2">Option 2</pop-select-option>
<pop-select-option value="3">Option 3</pop-select-option>
<pop-select-option value="4">Option 4</pop-select-option>
</pop-select>
<pop-select placeholder="Select a value">
<span slot="label">Label</span>
<pop-select-option value="1">Option 1</pop-select-option>
<pop-select-option value="2">Option 2</pop-select-option>
<pop-select-option value="3">Option 3</pop-select-option>
<pop-select-option value="4">Option 4</pop-select-option>
</pop-select>
<pop-select placeholder="Select a value">
<span slot="label">Label</span>
<pop-select-option value="1">Option 1</pop-select-option>
<pop-select-option value="2">Option 2</pop-select-option>
<pop-select-option value="3">Option 3</pop-select-option>
<pop-select-option value="4">Option 4</pop-select-option>
</pop-select>
</div>
</section>
<section>
<h2>Select - joined</h2>
<div>
<pop-join>
<pop-select placeholder="Select a value">
<span slot="label">Label</span>
<pop-select-option value="1">Option 1</pop-select-option>
<pop-select-option value="2">Option 2</pop-select-option>
<pop-select-option value="3">Option 3</pop-select-option>
<pop-select-option value="4">Option 4</pop-select-option>
</pop-select>
<pop-select placeholder="Select a value">
<span slot="label">Label</span>
<pop-select-option value="1">Option 1</pop-select-option>
<pop-select-option value="2">Option 2</pop-select-option>
<pop-select-option value="3">Option 3</pop-select-option>
<pop-select-option value="4">Option 4</pop-select-option>
</pop-select>
<pop-select placeholder="Select a value">
<span slot="label">Label</span>
<pop-select-option value="1">Option 1</pop-select-option>
<pop-select-option value="2">Option 2</pop-select-option>
<pop-select-option value="3">Option 3</pop-select-option>
<pop-select-option value="4">Option 4</pop-select-option>
</pop-select>
</pop-join>
</div>
</section>
</main>
</body>

</html>

0 comments on commit 59887c9

Please sign in to comment.