Skip to content

Commit

Permalink
refactor(component): cleaning up unused props & integrating into head…
Browse files Browse the repository at this point in the history
…less page

Component is named AltSelect (alt-select in path)  to avoid naming conflict with existing headless
implementation.

re qwikifiers#76
  • Loading branch information
shiroinegai committed Jan 24, 2023
1 parent bb7c5ca commit a2b9d50
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 308 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,90 +28,76 @@ interface StyleProps {

interface RootProps extends StyleProps {
defaultValue?: string;
placeholder?: string;
name?: string;
disabled?: boolean;
required?: boolean;
}

const Root = component$(
({ defaultValue, name, disabled, required, ...props }: RootProps) => {
const selectedOption = useSignal(defaultValue ? defaultValue : '');
const isExpanded = useSignal(false);

const triggerRef = useSignal<HTMLElement>();
const setTriggerRef$ = $((ref: Signal<HTMLElement | undefined>) => {
if (ref) {
triggerRef.value = ref.value;
}
});

const listBoxRef = useSignal<HTMLElement>();
const setListBoxRef$ = $((ref: Signal<HTMLElement | undefined>) => {
if (ref) {
listBoxRef.value = ref.value;
}
});

const contextService: SelectRootContextService = {
selectedOption,
isExpanded,
setTriggerRef$,
setListBoxRef$,
};

useContextProvider(selectContext, contextService);

useClientEffect$(async ({ track }) => {
const trigger = track(() => triggerRef.value);
const listBox = track(() => listBoxRef.value);
const expanded = track(() => isExpanded.value);

if (expanded && trigger && listBox) {
computePosition(trigger, listBox, {
placement: 'bottom',
middleware: [flip()],
}).then(({ x, y }) => {
Object.assign(listBox.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}
const Root = component$(({ defaultValue, ...props }: RootProps) => {
const selectedOption = useSignal(defaultValue ? defaultValue : '');
const isExpanded = useSignal(false);

if (expanded === false) {
trigger?.focus();
}
});
const triggerRef = useSignal<HTMLElement>();
const setTriggerRef$ = $((ref: Signal<HTMLElement | undefined>) => {
if (ref) {
triggerRef.value = ref.value;
}
});

return (
<div
onKeyUp$={(e) => {
const target = e.target as HTMLElement;
const listBoxRef = useSignal<HTMLElement>();
const setListBoxRef$ = $((ref: Signal<HTMLElement | undefined>) => {
if (ref) {
listBoxRef.value = ref.value;
}
});

if (e.key === 'Escape') {
contextService.isExpanded.value = false;
}
if (
(e.key === 'Enter' || e.key === ' ') &&
target.getAttribute('value')
) {
const value = target.getAttribute('value') as string;
selectedOption.value = value;
contextService.isExpanded.value = false;
}
}}
{...props}
>
<Slot />
</div>
);
}
);
const contextService: SelectRootContextService = {
selectedOption,
isExpanded,
setTriggerRef$,
setListBoxRef$,
};

useContextProvider(selectContext, contextService);

useClientEffect$(async ({ track }) => {
const trigger = track(() => triggerRef.value);
const listBox = track(() => listBoxRef.value);
const expanded = track(() => isExpanded.value);

if (expanded && trigger && listBox) {
computePosition(trigger, listBox, {
placement: 'bottom',
middleware: [flip()],
}).then(({ x, y }) => {
Object.assign(listBox.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}

interface TriggerProps extends StyleProps {}
if (expanded === false) {
trigger?.focus();
}
});

const Trigger = component$(({ ...props }: TriggerProps) => {
return (
<div
onKeyUp$={(e) => {
if (e.key === 'Escape') {
contextService.isExpanded.value = false;
}
}}
{...props}
>
<Slot />
</div>
);
});

interface TriggerProps extends StyleProps {
disabled?: boolean;
}

const Trigger = component$(({ disabled, ...props }: TriggerProps) => {
const ref = useSignal<HTMLElement>();
const contextService = useContext(selectContext);

Expand All @@ -123,6 +109,7 @@ const Trigger = component$(({ ...props }: TriggerProps) => {
<button
ref={ref}
aria-expanded={contextService.isExpanded.value}
disabled={disabled}
onClick$={() => {
contextService.isExpanded.value = !contextService.isExpanded.value;
}}
Expand All @@ -147,7 +134,7 @@ interface MarkerProps extends StyleProps {}

const Marker = component$(({ ...props }: MarkerProps) => {
return (
<span {...props}>
<span aria-hidden="true" {...props}>
<Slot />
</span>
);
Expand Down Expand Up @@ -186,7 +173,7 @@ interface GroupProps extends StyleProps {

const Group = component$(({ disabled, ...props }: GroupProps) => {
return (
<div role="group" {...props}>
<div role="group" aria-disabled={disabled} {...props}>
<Slot />
</div>
);
Expand Down Expand Up @@ -215,14 +202,35 @@ const Option = component$(
return (
<li
role="option"
tabIndex={0}
tabIndex={disabled ? -1 : 0}
value={value}
aria-disabled={disabled}
aria-selected={value === contextService.selectedOption.value}
onClick$={(e) => {
if (!disabled) {
const target = e.target as HTMLElement;
const value = target.getAttribute('value') as string;
contextService.selectedOption.value = value;
contextService.isExpanded.value = false;
}
}}
onKeyUp$={(e) => {
const target = e.target as HTMLElement;
const value = target.getAttribute('value') as string;
contextService.selectedOption.value = value;
contextService.isExpanded.value = false;
if (
!disabled &&
(e.key === 'Enter' || e.key === ' ') &&
target.getAttribute('value')
) {
const value = target.getAttribute('value') as string;
contextService.selectedOption.value = value;
contextService.isExpanded.value = false;
}
}}
onMouseEnter$={(e) => {
if (!disabled) {
const target = e.target as HTMLElement;
target.focus();
}
}}
{...props}
>
Expand Down
2 changes: 1 addition & 1 deletion packages/headless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export * from './components/tabs/tabs';
export * from './components/toggle/toggle';
export * from './components/tooltip/tooltip';
export * from './components/select/select';
export * as SNSelect from './components/select/sn-select';
export * as AltSelect from './components/select/alt-select';
63 changes: 62 additions & 1 deletion packages/website/src/routes/headless-example/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,65 @@
display: flex;
justify-content: center;
align-items: center;
}
}

/* AltSelect styles - start */
.qui-SelectRoot {
--qui-SelectSpace: 12px;
background: white;
color: black;
position: relative;
width: 100%;
}

.qui-SelectTrigger {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--qui-SelectSpace) calc(2 * var(--qui-SelectSpace));
border: 1px solid grey;
background: inherit;
width: inherit;
}

.qui-SelectMarker > *:only-child {
width: 1.5em;
height: 1.5em;
}

.qui-SelectListBox {
background: inherit;
border: 1px solid grey;
margin: 0;
padding: 0;
width: inherit;
}

.qui-SelectGroup .qui-SelectLabel {
display: block;
background: lightgrey;
font-weight: bold;
font-style: italic;
padding: calc(0.5 * var(--qui-SelectSpace)) calc(2 * var(--qui-SelectSpace));
}

.qui-SelectOption {
padding: var(--qui-SelectSpace) calc(2 * var(--qui-SelectSpace));
list-style: none;
}

.qui-SelectOption[aria-disabled='true'] {
color: grey;
}

.qui-SelectOption:hover {
background: var(--qwik-light-blue);
color: white;
}

.qui-SelectOption[aria-disabled='true']:hover {
background: unset;
color: grey;
cursor: not-allowed;
}
/* AltSelect styles - end */
53 changes: 53 additions & 0 deletions packages/website/src/routes/headless-example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TabPanel,
Tabs,
Tooltip,
AltSelect,
} from '@qwik-ui/headless';
import styles from './index.css?inline';

Expand Down Expand Up @@ -43,6 +44,58 @@ export default component$(() => {
})}
</SelectOptionsList>
</Select>
<AltSelect.Root class="qui-SelectRoot">
<AltSelect.Label class="qui-SelectLabel">
Alternate Headless Select implementation (minimally styled)
</AltSelect.Label>
<AltSelect.Trigger class="qui-SelectTrigger">
<AltSelect.Value placeholder="Select an option! ⚡" />
<AltSelect.Marker class="qui-SelectMarker">
{/* from feather icons */}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</AltSelect.Marker>
</AltSelect.Trigger>
<AltSelect.ListBox class="qui-SelectListBox">
<AltSelect.Option
label="🚀 Qwik"
value="🚀 Qwik"
class="qui-SelectOption"
/>
<AltSelect.Group class="qui-SelectGroup">
<AltSelect.Label class="qui-SelectLabel">Fruits</AltSelect.Label>
{[
{ label: '🍎 Apple', value: '🍎 Apple', disabled: false },
{ label: '🍌 Banana', value: '🍌 Banana', disabled: false },
{ label: '🍒 Cherry', value: '🍒 Cherry', disabled: false },
{
label: '🐲 Dragonfruit',
value: '🐲 Dragonfruit',
disabled: true,
},
].map((option) => {
return (
<AltSelect.Option
key={useId()}
label={option.label}
value={option.value}
disabled={option.disabled}
class="qui-SelectOption"
/>
);
})}
</AltSelect.Group>
</AltSelect.ListBox>
</AltSelect.Root>
<p style="position: relative;">Hey Shai!</p>
<Tabs behavior="automatic">
<h3 id="tablist-1">Danish Composers</h3>
Expand Down
Loading

0 comments on commit a2b9d50

Please # to comment.