mirror of
https://github.com/zoriya/v10.git
synced 2026-08-14 09:59:44 +00:00
docs(site): add Slider and Tooltip API reference pages (#862)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
487be3ea96
commit
7a5ce94bca
@@ -121,7 +121,9 @@ Toggles mute on and off. Exposes a derived `volumeLevel` based on the current vo
|
||||
|
||||
### Styling
|
||||
|
||||
Show data attributes as a table, then CSS selector patterns:
|
||||
**IMPORTANT:** All CSS code blocks in Styling sections MUST be wrapped in `<FrameworkCase>` blocks. HTML examples use custom element selectors (`media-mute-button`), React examples use className-based selectors (`.mute-button`). Never show bare CSS without a framework wrapper — React users should not see HTML element selectors and vice versa.
|
||||
|
||||
Show data attributes as a table, then framework-specific CSS selector patterns:
|
||||
|
||||
```mdx
|
||||
## Styling
|
||||
@@ -133,9 +135,19 @@ Show data attributes as a table, then CSS selector patterns:
|
||||
|
||||
Use `data-volume-level` for multi-level icon switching:
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```css
|
||||
media-mute-button[data-volume-level="off"] .icon-off { display: inline; }
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders standard DOM elements with the same data attributes. Add a `className` and use it as the selector:
|
||||
|
||||
```css
|
||||
.mute-button[data-volume-level="off"] .icon-off { display: inline; }
|
||||
```
|
||||
</FrameworkCase>
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
|
||||
@@ -325,9 +325,9 @@ always produce multi-part output since the re-exports are resolved rather than f
|
||||
|
||||
Every multi-part component has one **primary part** and one or more **sub-parts**.
|
||||
|
||||
**Primary part:** The part whose React source file instantiates the component's Core class
|
||||
(matches `new \w+Core\(`). This captures the architectural relationship — the primary part
|
||||
owns the Core — and is immune to import ordering and framework-divergent element structures.
|
||||
**Primary part:** The part whose React source file instantiates the component's own Core class
|
||||
(matches `new {ComponentName}Core\b`). This captures the architectural relationship — the primary
|
||||
part owns the Core — and is immune to import ordering and framework-divergent element structures.
|
||||
|
||||
Sub-part element files use the naming convention `{component}-{part}-element.ts` (e.g.,
|
||||
`time-group-element.ts`) for HTML tag resolution.
|
||||
@@ -340,6 +340,10 @@ For each local named export in `index.parts.ts`:
|
||||
3. If found → sub-part (gets its own tag name)
|
||||
4. If not found AND `{component}-element.ts` exists → check via Core-instantiation for primary
|
||||
|
||||
For cases where the element file doesn't follow standard naming (e.g., Tooltip's Provider maps
|
||||
to `tooltip-group-element.ts`, not `tooltip-provider-element.ts`), a `PART_ELEMENT_OVERRIDES`
|
||||
map provides the correct filename.
|
||||
|
||||
For re-exported parts: use the origin component's kebab and HTML directory for element file
|
||||
lookup. The element class name is derived from the filename convention (`kebabToPascal` of the
|
||||
basename, e.g., `slider-buffer-element.ts` → `SliderBufferElement`), not from the current
|
||||
@@ -351,9 +355,9 @@ properties, and the root element's tag name.
|
||||
|
||||
**What sub-parts get:** Their own tag name, a description (from React JSDoc), shared data
|
||||
attributes from the component's `*-data-attrs.ts` file (when the sub-part's React source
|
||||
references `stateAttrMap`), and custom React-specific props (own members on the
|
||||
`{LocalName}Props` interface, excluding inherited `UIComponentProps` members and `children`).
|
||||
State and CSS custom properties remain empty.
|
||||
references `stateAttrMap`), and custom props from the `{LocalName}Props` interface — own
|
||||
members plus members inherited from project-local interfaces, excluding `children` and
|
||||
React DOM attributes. State and CSS custom properties remain empty.
|
||||
|
||||
For re-exported sub-parts, data attributes come from the **origin** component's data-attrs file
|
||||
(e.g., TimeSlider.Fill uses Slider's data-attrs, not TimeSlider's), because the builder can't
|
||||
@@ -910,6 +914,13 @@ HTML sees the tag name (e.g., "media-meter-track"). The TOC emits both variants
|
||||
framework supports (derived from `platforms` keys). React-only parts (those with
|
||||
`platforms.react` but no `platforms.html`) are hidden when viewing HTML docs.
|
||||
|
||||
**Part ordering:** By default, parts render in JSON key order (primary first, then discovery
|
||||
order). The `<ComponentReference>` component accepts an optional `partOrder` prop — an array
|
||||
of part IDs (e.g., `["provider", "root", "trigger", "popup", "arrow"]`) that overrides
|
||||
the default order. This lets MDX authors match the anatomy. The reordering is applied inside
|
||||
`createComponentReferenceModel`, so both the rendered output and the TOC consume the same
|
||||
order. Parts not listed in `partOrder` appear after the listed ones in their original order.
|
||||
|
||||
### 5b. Util reference model
|
||||
|
||||
**Single-overload** heading structure:
|
||||
@@ -938,7 +949,10 @@ H2 "API Reference" id="api-reference"
|
||||
The `remarkConditionalHeadings` remark plugin detects `<ComponentReference>` and
|
||||
`<UtilReference>` components in MDX, loads the generated JSON, builds the reference model, and
|
||||
injects synthetic heading entries into `frontmatter.conditionalHeadings`. These entries carry the
|
||||
same `id`/`slug` values as the rendered headings, so TOC links always match.
|
||||
same `id`/`slug` values as the rendered headings, so TOC links always match. For
|
||||
`<ComponentReference>`, the plugin also reads the optional `partOrder` attribute and forwards
|
||||
it to `createComponentReferenceModel`, ensuring the TOC reflects the same part ordering as
|
||||
the rendered page.
|
||||
|
||||
---
|
||||
|
||||
@@ -1146,6 +1160,13 @@ H3: Part name (framework-specific label)
|
||||
H4: CSS custom properties (if non-empty) → CSS custom properties table
|
||||
```
|
||||
|
||||
**Part ordering:** Parts render in JSON key order by default (primary part first). To match
|
||||
the component anatomy, pass `partOrder` on the `<ComponentReference>` component:
|
||||
|
||||
```mdx
|
||||
<ComponentReference component="Tooltip" partOrder={["provider", "root", "trigger", "popup", "arrow"]} />
|
||||
```
|
||||
|
||||
**State section preamble** (framework-specific):
|
||||
|
||||
- **React:** "State is accessible via the `render`, `className`, and `style` props."
|
||||
|
||||
@@ -30,6 +30,12 @@ const NAME_OVERRIDES: Record<string, string> = {
|
||||
'pip-button': 'PiPButton',
|
||||
};
|
||||
|
||||
// Parts whose HTML element file doesn't follow the `{component}-{part}-element.ts` convention.
|
||||
// Key: `{component}/{part-kebab}`, Value: element file basename (without `.ts`).
|
||||
const PART_ELEMENT_OVERRIDES: Record<string, string> = {
|
||||
'tooltip/provider': 'tooltip-group-element',
|
||||
};
|
||||
|
||||
function buildProps(coreData: CoreExtraction): Record<string, PropDef> {
|
||||
const props: Record<string, PropDef> = {};
|
||||
for (const prop of coreData.props) {
|
||||
@@ -298,12 +304,14 @@ function buildSingleComponentReference(source: ComponentSource, program: ts.Prog
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a React source file instantiates a Core class (matches `new \w+Core\(`).
|
||||
* Check if a React source file instantiates the component's own Core class
|
||||
* (matches `new {ComponentName}Core(`). This prevents auxiliary classes like
|
||||
* `TooltipGroupCore` from being mistaken for the primary Core.
|
||||
*/
|
||||
function instantiatesCore(filePath: string): boolean {
|
||||
function instantiatesCore(filePath: string, componentName: string): boolean {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return /new \w+Core\(/.test(content);
|
||||
return new RegExp(`new ${componentName}Core\\b`).test(content);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -349,8 +357,10 @@ function discoverParts(source: ComponentSource, program: ts.Program): PartSource
|
||||
for (const partExport of localExports) {
|
||||
const kebab = partKebabFromSource(partExport.source, componentKebab);
|
||||
|
||||
// Look for sub-part element file: {component}-{part}-element.ts
|
||||
const subPartElementFile = path.join(htmlDir, `${componentKebab}-${kebab}-element.ts`);
|
||||
// Look for sub-part element file: {component}-{part}-element.ts (or override)
|
||||
const overrideKey = `${componentKebab}/${kebab}`;
|
||||
const elementBasename = PART_ELEMENT_OVERRIDES[overrideKey] ?? `${componentKebab}-${kebab}-element`;
|
||||
const subPartElementFile = path.join(htmlDir, `${elementBasename}.ts`);
|
||||
const hasSubPartElement = fs.existsSync(subPartElementFile);
|
||||
|
||||
// Resolve React source path for JSDoc description extraction
|
||||
@@ -358,7 +368,7 @@ function discoverParts(source: ComponentSource, program: ts.Program): PartSource
|
||||
const reactPath = fs.existsSync(reactFile) ? reactFile : undefined;
|
||||
|
||||
// Primary detection: the part whose React source instantiates the Core class
|
||||
const isPrimary = !!reactPath && instantiatesCore(reactPath);
|
||||
const isPrimary = !!reactPath && instantiatesCore(reactPath, source.name);
|
||||
|
||||
const subPartUsesDataAttrs = !isPrimary && !!reactPath && usesDataAttrs(reactPath);
|
||||
|
||||
|
||||
@@ -77,8 +77,10 @@ export function extractPartDescription(filePath: string, program: ts.Program, pa
|
||||
/**
|
||||
* Extract custom React-specific props from a sub-part's Props interface.
|
||||
*
|
||||
* Walks syntactic own members of `{localName}Props` (excluding inherited
|
||||
* `UIComponentProps` members and `children`).
|
||||
* Walks syntactic own members of `{localName}Props`, then also includes
|
||||
* members from any extended interface declared within the project (i.e., not
|
||||
* from `node_modules`). This picks up props from project types like
|
||||
* `TooltipGroupProps` while excluding inherited React DOM attributes.
|
||||
*/
|
||||
export function extractSubPartProps(filePath: string, program: ts.Program, localName: string): Record<string, PropDef> {
|
||||
const sourceFile = program.getSourceFile(filePath);
|
||||
@@ -86,27 +88,53 @@ export function extractSubPartProps(filePath: string, program: ts.Program, local
|
||||
const checker = program.getTypeChecker();
|
||||
const props: Record<string, PropDef> = {};
|
||||
|
||||
const SKIP_PROPS = new Set(['children']);
|
||||
|
||||
function collectFromMembers(members: ts.NodeArray<ts.TypeElement>) {
|
||||
for (const member of members) {
|
||||
if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name)) continue;
|
||||
const name = member.name.text;
|
||||
if (SKIP_PROPS.has(name) || !member.type) continue;
|
||||
|
||||
let typeStr = checker.typeToString(checker.getTypeFromTypeNode(member.type));
|
||||
if (member.questionToken) typeStr = typeStr.replace(/ \| undefined$/, '');
|
||||
|
||||
const propDef: PropDef = { type: typeStr };
|
||||
|
||||
const symbol = checker.getSymbolAtLocation(member.name);
|
||||
if (symbol) {
|
||||
const docs = symbol.getDocumentationComment(checker);
|
||||
const desc = docs.map((d) => d.text).join('');
|
||||
if (desc) propDef.description = desc;
|
||||
}
|
||||
|
||||
props[name] = propDef;
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(sourceFile, function visit(node) {
|
||||
if (ts.isInterfaceDeclaration(node) && node.name.text === `${localName}Props`) {
|
||||
for (const member of node.members) {
|
||||
if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name)) continue;
|
||||
const name = member.name.text;
|
||||
if (name === 'children' || !member.type) continue;
|
||||
// Collect own syntactic members.
|
||||
collectFromMembers(node.members);
|
||||
|
||||
let typeStr = checker.typeToString(checker.getTypeFromTypeNode(member.type));
|
||||
// Only strips trailing ` | undefined`; other orderings (e.g., `undefined | string`) pass through.
|
||||
if (member.questionToken) typeStr = typeStr.replace(/ \| undefined$/, '');
|
||||
// Walk extends clause and include members from project-local interfaces.
|
||||
if (node.heritageClauses) {
|
||||
for (const clause of node.heritageClauses) {
|
||||
for (const expr of clause.types) {
|
||||
const type = checker.getTypeAtLocation(expr);
|
||||
const symbol = type.getSymbol();
|
||||
const decl = symbol?.declarations?.[0];
|
||||
if (!decl) continue;
|
||||
|
||||
const propDef: PropDef = { type: typeStr };
|
||||
// Only include if declared in project sources (not node_modules).
|
||||
const declFile = decl.getSourceFile().fileName;
|
||||
if (declFile.includes('node_modules')) continue;
|
||||
|
||||
const symbol = checker.getSymbolAtLocation(member.name);
|
||||
if (symbol) {
|
||||
const docs = symbol.getDocumentationComment(checker);
|
||||
const desc = docs.map((d) => d.text).join('');
|
||||
if (desc) propDef.description = desc;
|
||||
if (ts.isInterfaceDeclaration(decl)) {
|
||||
collectFromMembers(decl.members);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
props[name] = propDef;
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
|
||||
@@ -19,9 +19,10 @@ import InlineMarkdown from './InlineMarkdown.astro';
|
||||
|
||||
interface Props {
|
||||
component: string;
|
||||
partOrder?: string[];
|
||||
}
|
||||
|
||||
const { component } = Astro.props;
|
||||
const { component, partOrder } = Astro.props;
|
||||
const { framework } = Astro.params;
|
||||
if (!framework || !isValidFramework(framework)) {
|
||||
throw new Error(`Invalid or missing framework param.`);
|
||||
@@ -31,7 +32,7 @@ const entry = await getEntry('componentReference', kebabCase(component));
|
||||
const apiRef: ComponentReference | null = entry?.data ?? null;
|
||||
if (!apiRef) return;
|
||||
|
||||
const apiReferenceModel = createComponentReferenceModel(component, apiRef);
|
||||
const apiReferenceModel = createComponentReferenceModel(component, apiRef, partOrder);
|
||||
if (!apiReferenceModel) return;
|
||||
const showAttributeName = framework === 'html';
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
|
||||
import html from './BasicUsage.html?raw';
|
||||
import './BasicUsage.css';
|
||||
---
|
||||
|
||||
<HtmlDemo html={html} />
|
||||
<script>
|
||||
import "./BasicUsage.ts";
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
.html-slider-basic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.html-slider-basic__slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.html-slider-basic__track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 9999px;
|
||||
transition: height 150ms ease;
|
||||
}
|
||||
|
||||
.html-slider-basic__slider[data-interactive] .html-slider-basic__track {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.html-slider-basic__fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: var(--media-slider-fill);
|
||||
background: white;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.html-slider-basic__thumb {
|
||||
position: absolute;
|
||||
left: var(--media-slider-fill);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%) scale(0);
|
||||
transition: transform 150ms ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.html-slider-basic__slider[data-interactive] .html-slider-basic__thumb {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
.html-slider-basic__slider[data-dragging] .html-slider-basic__thumb {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="html-slider-basic">
|
||||
<media-slider class="html-slider-basic__slider" value="50">
|
||||
<media-slider-track class="html-slider-basic__track">
|
||||
<media-slider-fill class="html-slider-basic__fill"></media-slider-fill>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="html-slider-basic__thumb"></media-slider-thumb>
|
||||
</media-slider>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
import '@videojs/html/ui/slider';
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
|
||||
import html from './WithPreview.html?raw';
|
||||
import './WithPreview.css';
|
||||
---
|
||||
|
||||
<HtmlDemo html={html} />
|
||||
<script>
|
||||
import "./WithPreview.ts";
|
||||
</script>
|
||||
@@ -0,0 +1,81 @@
|
||||
.html-slider-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40px 24px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.html-slider-preview__slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.html-slider-preview__track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 9999px;
|
||||
transition: height 150ms ease;
|
||||
}
|
||||
|
||||
.html-slider-preview__slider[data-interactive] .html-slider-preview__track {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.html-slider-preview__fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: var(--media-slider-fill);
|
||||
background: white;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.html-slider-preview__thumb {
|
||||
position: absolute;
|
||||
left: var(--media-slider-fill);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%) scale(0);
|
||||
transition: transform 150ms ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.html-slider-preview__slider[data-interactive] .html-slider-preview__thumb {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
.html-slider-preview__slider[data-dragging] .html-slider-preview__thumb {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.html-slider-preview__preview {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
margin-bottom: 6px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.html-slider-preview__slider[data-pointing] .html-slider-preview__preview {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.html-slider-preview__value {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="html-slider-preview">
|
||||
<media-slider class="html-slider-preview__slider" value="50">
|
||||
<media-slider-track class="html-slider-preview__track">
|
||||
<media-slider-fill class="html-slider-preview__fill"></media-slider-fill>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="html-slider-preview__thumb"></media-slider-thumb>
|
||||
<media-slider-preview class="html-slider-preview__preview">
|
||||
<media-slider-value type="pointer" class="html-slider-preview__value"></media-slider-value>
|
||||
</media-slider-preview>
|
||||
</media-slider>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
import '@videojs/html/ui/slider';
|
||||
@@ -0,0 +1,59 @@
|
||||
.react-slider-basic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.react-slider-basic__slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-slider-basic__track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 9999px;
|
||||
transition: height 150ms ease;
|
||||
}
|
||||
|
||||
.react-slider-basic__slider[data-interactive] .react-slider-basic__track {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.react-slider-basic__fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: var(--media-slider-fill);
|
||||
background: white;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.react-slider-basic__thumb {
|
||||
position: absolute;
|
||||
left: var(--media-slider-fill);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%) scale(0);
|
||||
transition: transform 150ms ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.react-slider-basic__slider[data-interactive] .react-slider-basic__thumb {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
.react-slider-basic__slider[data-dragging] .react-slider-basic__thumb {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Slider } from '@videojs/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import './BasicUsage.css';
|
||||
|
||||
export default function BasicUsage() {
|
||||
const [value, setValue] = useState(50);
|
||||
|
||||
return (
|
||||
<div className="react-slider-basic">
|
||||
<Slider.Root className="react-slider-basic__slider" value={value} onValueChange={setValue}>
|
||||
<Slider.Track className="react-slider-basic__track">
|
||||
<Slider.Fill className="react-slider-basic__fill" />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb className="react-slider-basic__thumb" />
|
||||
</Slider.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
.react-slider-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40px 24px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.react-slider-preview__slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-slider-preview__track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 9999px;
|
||||
transition: height 150ms ease;
|
||||
}
|
||||
|
||||
.react-slider-preview__slider[data-interactive] .react-slider-preview__track {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.react-slider-preview__fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: var(--media-slider-fill);
|
||||
background: white;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.react-slider-preview__thumb {
|
||||
position: absolute;
|
||||
left: var(--media-slider-fill);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%) scale(0);
|
||||
transition: transform 150ms ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.react-slider-preview__slider[data-interactive] .react-slider-preview__thumb {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
.react-slider-preview__slider[data-dragging] .react-slider-preview__thumb {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.react-slider-preview__preview {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
margin-bottom: 6px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.react-slider-preview__slider[data-pointing] .react-slider-preview__preview {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.react-slider-preview__value {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Slider } from '@videojs/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import './WithPreview.css';
|
||||
|
||||
export default function WithPreview() {
|
||||
const [value, setValue] = useState(50);
|
||||
|
||||
return (
|
||||
<div className="react-slider-preview">
|
||||
<Slider.Root className="react-slider-preview__slider" value={value} onValueChange={setValue}>
|
||||
<Slider.Track className="react-slider-preview__track">
|
||||
<Slider.Fill className="react-slider-preview__fill" />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb className="react-slider-preview__thumb" />
|
||||
<Slider.Preview className="react-slider-preview__preview">
|
||||
<Slider.Value type="pointer" className="react-slider-preview__value" />
|
||||
</Slider.Preview>
|
||||
</Slider.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
|
||||
import html from './BasicUsage.html?raw';
|
||||
import './BasicUsage.css';
|
||||
---
|
||||
|
||||
<HtmlDemo html={html} />
|
||||
<script>
|
||||
import "./BasicUsage.ts";
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
.html-tooltip-basic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.html-tooltip-basic__trigger {
|
||||
padding: 6px 16px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
color: black;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 9999px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.html-tooltip-basic__popup {
|
||||
--media-tooltip-side-offset: 8px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="html-tooltip-basic">
|
||||
<button type="button" commandfor="tooltip-demo" class="html-tooltip-basic__trigger">Hover me</button>
|
||||
<media-tooltip id="tooltip-demo" class="html-tooltip-basic__popup">
|
||||
Tooltip content
|
||||
</media-tooltip>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
import '@videojs/html/ui/tooltip';
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
|
||||
import html from './Grouping.html?raw';
|
||||
import './Grouping.css';
|
||||
---
|
||||
|
||||
<HtmlDemo html={html} />
|
||||
<script>
|
||||
import "./Grouping.ts";
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
.html-tooltip-grouping {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.html-tooltip-grouping__trigger {
|
||||
padding: 6px 16px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
color: black;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.html-tooltip-grouping__popup {
|
||||
--media-tooltip-side-offset: 8px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<media-tooltip-group class="html-tooltip-grouping">
|
||||
<button type="button" commandfor="tooltip-play" class="html-tooltip-grouping__trigger">Play</button>
|
||||
<media-tooltip id="tooltip-play" class="html-tooltip-grouping__popup">Play video</media-tooltip>
|
||||
|
||||
<button type="button" commandfor="tooltip-mute" class="html-tooltip-grouping__trigger">Mute</button>
|
||||
<media-tooltip id="tooltip-mute" class="html-tooltip-grouping__popup">Mute audio</media-tooltip>
|
||||
|
||||
<button type="button" commandfor="tooltip-fullscreen" class="html-tooltip-grouping__trigger">Fullscreen</button>
|
||||
<media-tooltip id="tooltip-fullscreen" class="html-tooltip-grouping__popup">Enter fullscreen</media-tooltip>
|
||||
</media-tooltip-group>
|
||||
@@ -0,0 +1,2 @@
|
||||
import '@videojs/html/ui/tooltip';
|
||||
import '@videojs/html/ui/tooltip-group';
|
||||
@@ -0,0 +1,34 @@
|
||||
.react-tooltip-basic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.react-tooltip-basic__trigger {
|
||||
padding: 6px 16px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
color: black;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 9999px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-tooltip-basic__popup {
|
||||
--media-tooltip-side-offset: 8px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.react-tooltip-basic__arrow {
|
||||
fill: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Tooltip } from '@videojs/react';
|
||||
|
||||
import './BasicUsage.css';
|
||||
|
||||
export default function BasicUsage() {
|
||||
return (
|
||||
<div className="react-tooltip-basic">
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger className="react-tooltip-basic__trigger">Hover me</Tooltip.Trigger>
|
||||
<Tooltip.Popup className="react-tooltip-basic__popup">
|
||||
<Tooltip.Arrow className="react-tooltip-basic__arrow" />
|
||||
Tooltip content
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
.react-tooltip-grouping {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.react-tooltip-grouping__trigger {
|
||||
padding: 6px 16px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
color: black;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-tooltip-grouping__popup {
|
||||
--media-tooltip-side-offset: 8px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.react-tooltip-grouping__arrow {
|
||||
fill: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Tooltip } from '@videojs/react';
|
||||
|
||||
import './Grouping.css';
|
||||
|
||||
export default function Grouping() {
|
||||
return (
|
||||
<div className="react-tooltip-grouping">
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger className="react-tooltip-grouping__trigger">Play</Tooltip.Trigger>
|
||||
<Tooltip.Popup className="react-tooltip-grouping__popup">
|
||||
<Tooltip.Arrow className="react-tooltip-grouping__arrow" />
|
||||
Play video
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger className="react-tooltip-grouping__trigger">Mute</Tooltip.Trigger>
|
||||
<Tooltip.Popup className="react-tooltip-grouping__popup">
|
||||
<Tooltip.Arrow className="react-tooltip-grouping__arrow" />
|
||||
Mute audio
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger className="react-tooltip-grouping__trigger">Fullscreen</Tooltip.Trigger>
|
||||
<Tooltip.Popup className="react-tooltip-grouping__popup">
|
||||
<Tooltip.Arrow className="react-tooltip-grouping__arrow" />
|
||||
Enter fullscreen
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -73,7 +73,7 @@ media-controls:not([data-visible]) {
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders `<div>` elements with the same data attributes. Add a `className` and use it as the selector:
|
||||
React renders `<div>` elements. Add a `className` to style them:
|
||||
|
||||
```css
|
||||
/* Click-through: clicks pass through controls to video beneath */
|
||||
|
||||
@@ -53,7 +53,7 @@ media-fullscreen-button[data-fullscreen] {
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders a `<button>` with the same data attributes. Add a `className` and use it as the selector:
|
||||
React renders a `<button>` element. Add a `className` to style it:
|
||||
|
||||
```css
|
||||
/* In fullscreen */
|
||||
|
||||
@@ -62,7 +62,7 @@ media-mute-button:not([data-muted]) .icon-unmuted { display: inline; }
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders a `<button>` with the same data attributes. Add a `className` and use it as the selector:
|
||||
React renders a `<button>` element. Add a `className` to style it:
|
||||
|
||||
```css
|
||||
.mute-button[data-muted] .icon-muted { display: inline; }
|
||||
|
||||
@@ -53,7 +53,7 @@ media-pip-button[data-pip] {
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders a `<button>` with the same data attributes. Add a `className` and use it as the selector:
|
||||
React renders a `<button>` element. Add a `className` to style it:
|
||||
|
||||
```css
|
||||
/* In PiP mode */
|
||||
|
||||
@@ -57,7 +57,7 @@ media-play-button[data-ended] .replay-icon { display: inline; }
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders a `<button>` with the same data attributes. Add a `className` and use it as the selector:
|
||||
React renders a `<button>` element. Add a `className` to style it:
|
||||
|
||||
```css
|
||||
/* Paused (but not ended) */
|
||||
|
||||
@@ -78,7 +78,7 @@ media-popover {
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders standard DOM elements with the same data attributes and CSS custom properties. Add a `className` and use it as the selector:
|
||||
React renders standard DOM elements. Add a `className` to style them:
|
||||
|
||||
```css
|
||||
.popover {
|
||||
|
||||
@@ -56,7 +56,7 @@ media-poster:not([data-visible]) {
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders an `<img>` with the same data attributes. Add a `className` and use it as the selector:
|
||||
React renders an `<img>` element. Add a `className` to style it:
|
||||
|
||||
```css
|
||||
.poster:not([data-visible]) {
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: Slider
|
||||
frameworkTitle:
|
||||
html: media-slider
|
||||
description: A composable slider component with track, fill, thumb, preview, and value parts
|
||||
---
|
||||
|
||||
import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
|
||||
import DocsLink from "@/components/docs/DocsLink.astro";
|
||||
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
|
||||
import StyleCase from "@/components/docs/StyleCase.astro";
|
||||
import Demo from "@/components/docs/demos/Demo.astro";
|
||||
|
||||
{/* React demos */}
|
||||
import BasicUsageDemoReact from "@/components/docs/demos/slider/react/css/BasicUsage";
|
||||
import basicUsageReactTsx from "@/components/docs/demos/slider/react/css/BasicUsage.tsx?raw";
|
||||
import basicUsageReactCss from "@/components/docs/demos/slider/react/css/BasicUsage.css?raw";
|
||||
|
||||
import WithPreviewDemoReact from "@/components/docs/demos/slider/react/css/WithPreview";
|
||||
import withPreviewReactTsx from "@/components/docs/demos/slider/react/css/WithPreview.tsx?raw";
|
||||
import withPreviewReactCss from "@/components/docs/demos/slider/react/css/WithPreview.css?raw";
|
||||
|
||||
{/* HTML demos */}
|
||||
import BasicUsageDemoHtml from "@/components/docs/demos/slider/html/css/BasicUsage.astro";
|
||||
import basicUsageHtml from "@/components/docs/demos/slider/html/css/BasicUsage.html?raw";
|
||||
import basicUsageHtmlCss from "@/components/docs/demos/slider/html/css/BasicUsage.css?raw";
|
||||
import basicUsageHtmlTs from "@/components/docs/demos/slider/html/css/BasicUsage.ts?raw";
|
||||
|
||||
import WithPreviewDemoHtml from "@/components/docs/demos/slider/html/css/WithPreview.astro";
|
||||
import withPreviewHtml from "@/components/docs/demos/slider/html/css/WithPreview.html?raw";
|
||||
import withPreviewHtmlCss from "@/components/docs/demos/slider/html/css/WithPreview.css?raw";
|
||||
import withPreviewHtmlTs from "@/components/docs/demos/slider/html/css/WithPreview.ts?raw";
|
||||
|
||||
## Anatomy
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
```tsx
|
||||
<Slider.Root>
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb />
|
||||
<Slider.Preview>
|
||||
<Slider.Value type="pointer" />
|
||||
</Slider.Preview>
|
||||
</Slider.Root>
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```html
|
||||
<media-slider>
|
||||
<media-slider-track>
|
||||
<media-slider-fill></media-slider-fill>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb></media-slider-thumb>
|
||||
<media-slider-preview>
|
||||
<media-slider-value type="pointer"></media-slider-value>
|
||||
</media-slider-preview>
|
||||
</media-slider>
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
## Behavior
|
||||
|
||||
The base Slider provides a generic range input. It manages value, pointer tracking, and drag interactions. Domain-specific sliders like <DocsLink slug="reference/time-slider">TimeSlider</DocsLink> and <DocsLink slug="reference/volume-slider">VolumeSlider</DocsLink> extend this with media-specific bindings.
|
||||
|
||||
The slider supports vertical orientation via the `orientation` prop (defaults to `"horizontal"`).
|
||||
|
||||
|
||||
## Styling
|
||||
|
||||
Use [CSS custom properties](#root-css-custom-properties) to position fill, thumb, and preview elements:
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```css
|
||||
media-slider-fill {
|
||||
width: var(--media-slider-fill);
|
||||
}
|
||||
|
||||
media-slider-thumb {
|
||||
left: var(--media-slider-fill);
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders standard DOM elements. Add a `className` to style them:
|
||||
|
||||
```css
|
||||
.slider-fill {
|
||||
width: var(--media-slider-fill);
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
left: var(--media-slider-fill);
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
Style based on [interaction state](#root-data-attributes):
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```css
|
||||
media-slider[data-interactive] media-slider-track {
|
||||
height: 6px;
|
||||
}
|
||||
media-slider[data-pointing] media-slider-preview {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
```css
|
||||
.slider[data-interactive] .slider-track {
|
||||
height: 6px;
|
||||
}
|
||||
.slider[data-pointing] .slider-preview {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
## Accessibility
|
||||
|
||||
Renders with `role="slider"` and automatic ARIA attributes (`aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext`). Override the label with the `label` prop. Keyboard controls:
|
||||
|
||||
- <kbd>Arrow Left</kbd> / <kbd>Arrow Right</kbd>: step by `step` increment
|
||||
- <kbd>Page Up</kbd> / <kbd>Page Down</kbd>: step by `largeStep` increment
|
||||
- <kbd>Home</kbd>: jump to minimum
|
||||
- <kbd>End</kbd>: jump to maximum
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
A slider with track, fill, and thumb.
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo
|
||||
files={[
|
||||
{ title: "App.tsx", code: basicUsageReactTsx, lang: "tsx" },
|
||||
{ title: "App.css", code: basicUsageReactCss, lang: "css" },
|
||||
]}
|
||||
>
|
||||
<BasicUsageDemoReact client:idle />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo
|
||||
files={[
|
||||
{ title: "index.html", code: basicUsageHtml, lang: "html" },
|
||||
{ title: "index.css", code: basicUsageHtmlCss, lang: "css" },
|
||||
{ title: "index.ts", code: basicUsageHtmlTs, lang: "ts" },
|
||||
]}
|
||||
>
|
||||
<BasicUsageDemoHtml />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
|
||||
### With Preview
|
||||
|
||||
A slider with a pointer-tracking preview that displays the value at the current pointer position.
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo
|
||||
files={[
|
||||
{ title: "App.tsx", code: withPreviewReactTsx, lang: "tsx" },
|
||||
{ title: "App.css", code: withPreviewReactCss, lang: "css" },
|
||||
]}
|
||||
>
|
||||
<WithPreviewDemoReact client:idle />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo
|
||||
files={[
|
||||
{ title: "index.html", code: withPreviewHtml, lang: "html" },
|
||||
{ title: "index.css", code: withPreviewHtmlCss, lang: "css" },
|
||||
{ title: "index.ts", code: withPreviewHtmlTs, lang: "ts" },
|
||||
]}
|
||||
>
|
||||
<WithPreviewDemoHtml />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
|
||||
<ComponentReference component="Slider" />
|
||||
@@ -116,7 +116,7 @@ media-thumbnail[data-error] {
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders a `<div>` with the same data attributes. Add a `className` and use it as the selector:
|
||||
React renders a `<div>` element. Add a `className` to style it:
|
||||
|
||||
```css
|
||||
.thumbnail[data-hidden] {
|
||||
|
||||
@@ -48,17 +48,17 @@ Use [CSS custom properties](#root-css-custom-properties) to style the fill, poin
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```css
|
||||
media-time-slider::before {
|
||||
width: calc(var(--media-slider-fill) * 1%);
|
||||
width: var(--media-slider-fill);
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders a `<div>` with the same data attributes and CSS custom properties. Add a `className` and use it as the selector:
|
||||
React renders a `<div>` element. Add a `className` to style it:
|
||||
|
||||
```css
|
||||
.time-slider::before {
|
||||
width: calc(var(--media-slider-fill) * 1%);
|
||||
width: var(--media-slider-fill);
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
---
|
||||
title: Tooltip
|
||||
frameworkTitle:
|
||||
html: media-tooltip
|
||||
description: A tooltip component for displaying contextual labels on hover and focus
|
||||
---
|
||||
|
||||
import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
|
||||
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
|
||||
import StyleCase from "@/components/docs/StyleCase.astro";
|
||||
import Demo from "@/components/docs/demos/Demo.astro";
|
||||
|
||||
{/* React demos */}
|
||||
import BasicUsageDemoReact from "@/components/docs/demos/tooltip/react/css/BasicUsage";
|
||||
import basicUsageReactTsx from "@/components/docs/demos/tooltip/react/css/BasicUsage.tsx?raw";
|
||||
import basicUsageReactCss from "@/components/docs/demos/tooltip/react/css/BasicUsage.css?raw";
|
||||
|
||||
import GroupingDemoReact from "@/components/docs/demos/tooltip/react/css/Grouping";
|
||||
import groupingReactTsx from "@/components/docs/demos/tooltip/react/css/Grouping.tsx?raw";
|
||||
import groupingReactCss from "@/components/docs/demos/tooltip/react/css/Grouping.css?raw";
|
||||
|
||||
{/* HTML demos */}
|
||||
import BasicUsageDemoHtml from "@/components/docs/demos/tooltip/html/css/BasicUsage.astro";
|
||||
import basicUsageHtml from "@/components/docs/demos/tooltip/html/css/BasicUsage.html?raw";
|
||||
import basicUsageHtmlCss from "@/components/docs/demos/tooltip/html/css/BasicUsage.css?raw";
|
||||
import basicUsageHtmlTs from "@/components/docs/demos/tooltip/html/css/BasicUsage.ts?raw";
|
||||
|
||||
import GroupingDemoHtml from "@/components/docs/demos/tooltip/html/css/Grouping.astro";
|
||||
import groupingHtml from "@/components/docs/demos/tooltip/html/css/Grouping.html?raw";
|
||||
import groupingHtmlCss from "@/components/docs/demos/tooltip/html/css/Grouping.css?raw";
|
||||
import groupingHtmlTs from "@/components/docs/demos/tooltip/html/css/Grouping.ts?raw";
|
||||
|
||||
## Anatomy
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
```tsx
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
||||
<Tooltip.Popup>
|
||||
<Tooltip.Arrow />
|
||||
Label text
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```html
|
||||
<media-tooltip-group>
|
||||
<button commandfor="my-tooltip">Hover me</button>
|
||||
<media-tooltip id="my-tooltip">Label text</media-tooltip>
|
||||
</media-tooltip-group>
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
## Behavior
|
||||
|
||||
Displays a short label anchored to a trigger element. Opens after a configurable `delay` (default 600ms) on hover or immediately on focus. Closes when the pointer leaves or focus moves away, with an optional `closeDelay`.
|
||||
|
||||
The `side` and `align` props control placement relative to the trigger. Positioning uses CSS Anchor Positioning where supported, with a JavaScript measurement fallback.
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
The component is composed from four parts: `Root` manages state and context,
|
||||
`Trigger` renders a button that activates the tooltip, `Popup` contains the label content,
|
||||
and `Arrow` renders a decorative pointer. Wrap multiple tooltips in a `Tooltip.Provider`
|
||||
to coordinate open/close timing across a group — once a tooltip becomes visible, adjacent
|
||||
tooltips open instantly within the `timeout` window, skipping the normal `delay`.
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
The `<media-tooltip>` element is the popup itself. Link it to a trigger using
|
||||
the `commandfor` attribute on any button, pointing to the tooltip's `id`. The element
|
||||
discovers its trigger automatically and manages open/close state, ARIA attributes, and
|
||||
positioning. Wrap tooltip trigger/popup pairs in `<media-tooltip-group>` to coordinate
|
||||
timing — the group's `delay`, `close-delay`, and `timeout` attributes control shared
|
||||
timing for all contained tooltips.
|
||||
</FrameworkCase>
|
||||
|
||||
## Styling
|
||||
|
||||
Use [CSS custom properties](#root-css-custom-properties) for positioning offsets:
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```css
|
||||
media-tooltip {
|
||||
--media-tooltip-side-offset: 8px;
|
||||
--media-tooltip-align-offset: 0px;
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders standard DOM elements. Add a `className` to style them:
|
||||
|
||||
```css
|
||||
.tooltip-popup {
|
||||
--media-tooltip-side-offset: 8px;
|
||||
--media-tooltip-align-offset: 0px;
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
Style based on open state and transition phases:
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
```css
|
||||
media-tooltip[data-open] {
|
||||
display: block;
|
||||
}
|
||||
media-tooltip[data-starting-style] {
|
||||
opacity: 0;
|
||||
}
|
||||
media-tooltip[data-ending-style] {
|
||||
opacity: 0;
|
||||
}
|
||||
media-tooltip[data-side="top"] {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
media-tooltip[data-side="bottom"] {
|
||||
transform-origin: top center;
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
```css
|
||||
.tooltip-popup[data-open] {
|
||||
display: block;
|
||||
}
|
||||
.tooltip-popup[data-starting-style] {
|
||||
opacity: 0;
|
||||
}
|
||||
.tooltip-popup[data-ending-style] {
|
||||
opacity: 0;
|
||||
}
|
||||
.tooltip-popup[data-side="top"] {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
.tooltip-popup[data-side="bottom"] {
|
||||
transform-origin: top center;
|
||||
}
|
||||
```
|
||||
</FrameworkCase>
|
||||
|
||||
## Accessibility
|
||||
|
||||
The trigger receives `aria-describedby` pointing to the popup when open. The popup renders with `role="tooltip"` and `popover="manual"`. Tooltips open on focus and close when focus leaves, ensuring keyboard-only users can access the label.
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo
|
||||
files={[
|
||||
{ title: "App.tsx", code: basicUsageReactTsx, lang: "tsx" },
|
||||
{ title: "App.css", code: basicUsageReactCss, lang: "css" },
|
||||
]}
|
||||
>
|
||||
<BasicUsageDemoReact client:idle />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo
|
||||
files={[
|
||||
{ title: "index.html", code: basicUsageHtml, lang: "html" },
|
||||
{ title: "index.css", code: basicUsageHtmlCss, lang: "css" },
|
||||
{ title: "index.ts", code: basicUsageHtmlTs, lang: "ts" },
|
||||
]}
|
||||
>
|
||||
<BasicUsageDemoHtml />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
|
||||
### Grouping
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
Wrap multiple tooltips in a `Tooltip.Provider` to share a delay group. Once a tooltip
|
||||
becomes visible, adjacent tooltips open instantly within the `timeout` window, skipping
|
||||
the normal `delay`.
|
||||
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo
|
||||
files={[
|
||||
{ title: "App.tsx", code: groupingReactTsx, lang: "tsx" },
|
||||
{ title: "App.css", code: groupingReactCss, lang: "css" },
|
||||
]}
|
||||
>
|
||||
<GroupingDemoReact client:idle />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
Wrap tooltip trigger/popup pairs in `<media-tooltip-group>` to coordinate timing. The
|
||||
group's `delay`, `close-delay`, and `timeout` attributes control shared timing for all
|
||||
contained tooltips.
|
||||
|
||||
<StyleCase styles={["css"]}>
|
||||
<Demo
|
||||
files={[
|
||||
{ title: "index.html", code: groupingHtml, lang: "html" },
|
||||
{ title: "index.css", code: groupingHtmlCss, lang: "css" },
|
||||
{ title: "index.ts", code: groupingHtmlTs, lang: "ts" },
|
||||
]}
|
||||
>
|
||||
<GroupingDemoHtml />
|
||||
</Demo>
|
||||
</StyleCase>
|
||||
</FrameworkCase>
|
||||
|
||||
<ComponentReference component="Tooltip" partOrder={["provider", "root", "trigger", "popup", "arrow"]} />
|
||||
@@ -52,7 +52,7 @@ media-volume-slider::before {
|
||||
</FrameworkCase>
|
||||
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
React renders a `<div>` with the same data attributes and CSS custom properties. Add a `className` and use it as the selector:
|
||||
React renders a `<div>` element. Add a `className` to style it:
|
||||
|
||||
```css
|
||||
.volume-slider::before {
|
||||
|
||||
@@ -55,9 +55,11 @@ export const sidebar: Sidebar = [
|
||||
{ slug: 'reference/popover' },
|
||||
{ slug: 'reference/poster' },
|
||||
{ slug: 'reference/seek-button' },
|
||||
{ slug: 'reference/slider' },
|
||||
{ slug: 'reference/thumbnail' },
|
||||
{ slug: 'reference/time' },
|
||||
{ slug: 'reference/time-slider' },
|
||||
{ slug: 'reference/tooltip' },
|
||||
{ slug: 'reference/volume-slider' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -82,7 +82,7 @@ function createSections(source, options) {
|
||||
* The shared model is what prevents anchor drift: ids are computed once and
|
||||
* reused verbatim by the renderer and the remark plugin.
|
||||
*/
|
||||
export function createComponentReferenceModel(componentName, apiReference) {
|
||||
export function createComponentReferenceModel(componentName, apiReference, partOrder) {
|
||||
if (!apiReference) {
|
||||
return null;
|
||||
}
|
||||
@@ -90,7 +90,18 @@ export function createComponentReferenceModel(componentName, apiReference) {
|
||||
const hasParts = Boolean(apiReference.parts && Object.keys(apiReference.parts).length > 0);
|
||||
|
||||
if (hasParts) {
|
||||
const parts = Object.entries(apiReference.parts).map(([partId, part]) => ({
|
||||
let partEntries = Object.entries(apiReference.parts);
|
||||
|
||||
if (partOrder) {
|
||||
const orderMap = new Map(partOrder.map((id, i) => [id, i]));
|
||||
partEntries = partEntries.slice().sort((a, b) => {
|
||||
const ai = orderMap.has(a[0]) ? orderMap.get(a[0]) : Number.MAX_SAFE_INTEGER;
|
||||
const bi = orderMap.has(b[0]) ? orderMap.get(b[0]) : Number.MAX_SAFE_INTEGER;
|
||||
return ai - bi;
|
||||
});
|
||||
}
|
||||
|
||||
const parts = partEntries.map(([partId, part]) => ({
|
||||
id: partId,
|
||||
name: part.name,
|
||||
description: part.description,
|
||||
|
||||
@@ -136,7 +136,10 @@ function injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSl
|
||||
const json = readComponentRefJson(componentName);
|
||||
if (!json) return;
|
||||
|
||||
const componentModel = createComponentReferenceModel(componentName, json);
|
||||
const partOrderAttr = node.attributes?.find((a) => a.name === 'partOrder');
|
||||
const partOrder = extractArrayValue(partOrderAttr);
|
||||
|
||||
const componentModel = createComponentReferenceModel(componentName, json, partOrder);
|
||||
const componentHeadings = buildComponentReferenceTocHeadings(componentModel);
|
||||
|
||||
headingsWithMetadata.push(...componentHeadings);
|
||||
|
||||
Reference in New Issue
Block a user