Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions apps/svelte.dev/src/lib/components/ModalDropdown.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { page } from '$app/stores';
import { focusable_children, trap } from '@sveltejs/site-kit/actions';
import { Icon } from '@sveltejs/site-kit/components';
import type { Snippet } from 'svelte';

let { children, label }: { children: Snippet; label: string } = $props();

let open = $state(false);

afterNavigate(() => {
open = false;
});
</script>

<svelte:window
onkeydown={(e) => {
if (e.key === 'Escape') {
open = false;
}
}}
/>

<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<details
class="examples-select"
bind:open
ontogglecapture={(e) => {
const details = e.target as HTMLDetailsElement;

if (details === e.currentTarget || !details.open) {
return;
}

details.scrollIntoView();
}}
ontoggle={(e) => {
const details = e.currentTarget;
if (!details.open) return;

// close all details elements...
for (const child of details.querySelectorAll('details[open]')) {
(child as HTMLDetailsElement).open = false;
}

// except parents of the current one
const current = details.querySelector(`[href="${$page.url.pathname}"]`) as HTMLAnchorElement | null;
if (!current) return;

let node = current as Element;

while ((node = (node.parentNode) as Element) && node !== details) {
if (node.nodeName === 'DETAILS') {
(node as HTMLDetailsElement).open = true;
}
}

current.scrollIntoView();
current.focus();
}}
onkeydown={(e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const children = focusable_children(e.currentTarget);

if (e.key === 'ArrowDown') {
children.next();
} else {
children.prev();
}
}

if (document.activeElement?.nodeName === 'SUMMARY' && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
(document.activeElement.parentNode as HTMLDetailsElement).open = e.key === 'ArrowRight';
}
}}
>
<summary class="raised icon" aria-label={label}><Icon name="menu" /></summary>

<div class="contents" use:trap>
{@render children()}
</div>
</details>

<style>
details {
position: relative;

&:has(:focus-visible) .raised.icon {
outline: 2px solid var(--sk-fg-accent);
border-radius: var(--sk-border-radius);
}

span {
pointer-events: none;
}
}

summary {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}

details[open] summary::before {
content: '';
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: grayscale(0.7) blur(3px);
z-index: 9998;
}

.contents {
position: absolute;
z-index: 9999;
background: var(--sk-bg-2);
padding: 1rem;
border-radius: var(--sk-border-radius);
filter: var(--sk-shadow);
max-height: calc(100vh - 16rem);
overflow-y: auto;
}

.icon {
position: relative;
color: var(--sk-fg-3);
line-height: 1;
background-size: 1.8rem;
z-index: 9999;
}

.icon:hover,
.icon:focus-visible {
opacity: 1;
}
</style>
60 changes: 0 additions & 60 deletions apps/svelte.dev/src/lib/components/SelectIcon.svelte

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import UserMenu from './UserMenu.svelte';
import { Icon } from '@sveltejs/site-kit/components';
import { isMac } from '$lib/utils/compat.js';
import { get_app_context } from '../../app-context';
import type { Gist, User } from '$lib/db/types';
import { browser } from '$app/environment';
import SelectIcon from '$lib/components/SelectIcon.svelte';
import ModalDropdown from '$lib/components/ModalDropdown.svelte';
import { untrack } from 'svelte';
import SecondaryNav from '$lib/components/SecondaryNav.svelte';
import type { File } from 'editor';
Expand Down Expand Up @@ -39,7 +39,7 @@
let saving = $state(false);
let justSaved = $state(false);
let justForked = $state(false);
let select: ReturnType<typeof SelectIcon>;
let select: ReturnType<typeof ModalDropdown>;

function wait(ms: number) {
return new Promise((f) => setTimeout(f, ms));
Expand Down Expand Up @@ -180,24 +180,40 @@
<svelte:window on:keydown={handleKeydown} />

<SecondaryNav>
<SelectIcon
bind:this={select}
title="examples"
value={gist.id}
onchange={async (e) => {
goto(`/playground/${e.currentTarget.value}`);
}}
>
<option value="untitled">Create new</option>
<ModalDropdown label="Examples">
<div class="secondary-nav-dropdown">
<a class="create-new" href="/playground/untitled">Create new</a>

{#each examples as section}
<details>
<summary>{section.title}</summary>

<ul>
{#each section.examples as example}
<li>
<a
href="/playground/{example.slug}"
aria-current={$page.params.id === example.slug ? 'page' : undefined}
>
{example.title}
</a>
</li>
{/each}
</ul>
</details>
{/each}
</div>

<!-- <option value="untitled">Create new</option>
<option disabled selected value="">or choose an example</option>
{#each examples as section}
<optgroup label={section.title}>
{#each section.examples as example}
<option value={example.slug}>{example.title}</option>
{/each}
</optgroup>
{/each}
</SelectIcon>
{/each} -->
</ModalDropdown>

<input
bind:value={name}
Expand Down Expand Up @@ -324,4 +340,8 @@
top: -0.2rem;
right: -0.2rem;
}

.create-new {
margin-bottom: 1rem;
}
</style>
56 changes: 32 additions & 24 deletions apps/svelte.dev/src/routes/tutorial/[...slug]/Controls.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import SecondaryNav from '$lib/components/SecondaryNav.svelte';
import SelectIcon from '$lib/components/SelectIcon.svelte';
import ModalDropdown from '$lib/components/ModalDropdown.svelte';
import type { Exercise, PartStub } from '$lib/tutorial';
import { Icon } from '@sveltejs/site-kit/components';

Expand All @@ -13,33 +13,37 @@
}

let { index, exercise, completed, toggle }: Props = $props();

// TODO this really sucks, why is `exercise.slug` not the slug?
let actual_slug = $derived.by(() => {
const parts = exercise.slug.split('/');
return `${parts[1].includes('kit') ? 'kit' : 'svelte'}/${parts[3]}`;
});
</script>

<SecondaryNav>
<SelectIcon
value={actual_slug}
onchange={(e) => {
goto(`/tutorial/${e.currentTarget.value}`);
}}
>
{#each index as part}
<optgroup label={part.title}>
{#each part.chapters as chapter}
<option disabled>{chapter.title}</option>
<ModalDropdown label="Menu">
<div class="secondary-nav-dropdown">
{#each index as part}
<details>
<summary>{part.title}</summary>

{#each part.chapters as chapter}
<details>
<summary>{chapter.title}</summary>

{#each chapter.exercises as exercise}
<option value={exercise.slug}>{exercise.title}</option>
<ul>
{#each chapter.exercises as exercise}
<li value={exercise.slug}>
<a
aria-current={$page.url.pathname === `/tutorial/${exercise.slug}`
? 'page'
: undefined}
href="/tutorial/{exercise.slug}">{exercise.title}</a
>
</li>
{/each}
</ul>
</details>
{/each}
{/each}
</optgroup>
{/each}
</SelectIcon>
</details>
{/each}
</div>
</ModalDropdown>

<a
href={exercise.prev ? `/tutorial/${exercise.prev?.slug}` : undefined}
Expand Down Expand Up @@ -77,6 +81,10 @@
opacity: 0.1;
cursor: default;
}

&[aria-current='page'] {
color: var(--sk-fg-accent);
}
}

.breadcrumbs {
Expand Down
8 changes: 3 additions & 5 deletions packages/site-kit/src/lib/actions/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ export function forcefocus(node: HTMLInputElement) {

export function focusable_children(node: HTMLElement) {
const nodes: HTMLElement[] = Array.from(
// this rather intimating selector selects elements that aren't children of closed <details> elements,
// except for the <summary> elements that are their direct children
node.querySelectorAll(
'a[href], button, input, textarea, select, summary, [tabindex]:not([tabindex="-1"])'
':where(a[href], button, input, textarea, select, summary, [tabindex]:not([tabindex="-1"])):not(details:not([open]) *), summary:not(details:not([open]) details *)'
)
);

Expand All @@ -31,10 +33,6 @@ export function focusable_children(node: HTMLElement) {
while ((node = reordered[i])) {
i += d;

if (node.matches('details:not([open]) *')) {
continue;
}

if (!selector || node.matches(selector)) {
node.focus();
return;
Expand Down
1 change: 1 addition & 0 deletions packages/site-kit/src/lib/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
@import './text.css';
@import './utils/buttons.css';
@import './utils/dividers.css';
@import './utils/nav.css';
@import './utils/twoslash.css';
Loading
Loading