@oneli8/core 1.0.0-beta.5 → 1.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -7
- package/ai-context.json +170 -3
- package/package.json +18 -3
- package/src/atoms/index.d.ts +1 -0
- package/src/atoms/index.js +1 -0
- package/src/atoms/navigation-badge/index.d.ts +1 -0
- package/src/atoms/navigation-badge/index.js +1 -0
- package/src/atoms/navigation-badge/navigation-badge.css +35 -0
- package/src/atoms/navigation-badge/navigation-badge.d.ts +5 -0
- package/src/atoms/navigation-badge/navigation-badge.js +21 -0
- package/src/atoms/selection-indicator/selection-indicator.css +13 -4
- package/src/materials/gem/gem.css +52 -5
- package/src/molecules/choice-chip/choice-chip.css +214 -0
- package/src/molecules/choice-chip/choice-chip.d.ts +27 -0
- package/src/molecules/choice-chip/choice-chip.js +170 -0
- package/src/molecules/choice-chip/index.d.ts +2 -0
- package/src/molecules/choice-chip/index.js +10 -0
- package/src/molecules/form-message/form-message.css +46 -0
- package/src/molecules/form-message/form-message.d.ts +23 -0
- package/src/molecules/form-message/form-message.js +73 -0
- package/src/molecules/form-message/index.d.ts +1 -0
- package/src/molecules/form-message/index.js +1 -0
- package/src/molecules/index.d.ts +6 -0
- package/src/molecules/index.js +6 -0
- package/src/molecules/link/index.d.ts +1 -0
- package/src/molecules/link/index.js +1 -0
- package/src/molecules/link/link.css +114 -0
- package/src/molecules/link/link.d.ts +27 -0
- package/src/molecules/link/link.js +76 -0
- package/src/molecules/navigation-item/index.js +1 -0
- package/src/molecules/navigation-item/navigation-item.css +153 -0
- package/src/molecules/navigation-item/navigation-item.js +95 -0
- package/src/molecules/select/index.js +1 -0
- package/src/molecules/select/select.css +31 -0
- package/src/molecules/select/select.js +127 -0
- package/src/molecules/selection-option/selection-option.js +4 -4
- package/src/molecules/text-field/index.d.ts +1 -0
- package/src/molecules/text-field/index.js +5 -0
- package/src/molecules/text-field/text-field.css +266 -0
- package/src/molecules/text-field/text-field.d.ts +69 -0
- package/src/molecules/text-field/text-field.js +234 -0
- package/src/molecules/token/index.d.ts +2 -0
- package/src/molecules/token/index.js +1 -0
- package/src/molecules/token/token.css +63 -0
- package/src/molecules/token/token.d.ts +35 -0
- package/src/molecules/token/token.js +83 -0
- package/src/organisms/choice-picker/choice-picker.css +98 -0
- package/src/organisms/choice-picker/choice-picker.d.ts +62 -0
- package/src/organisms/choice-picker/choice-picker.js +201 -0
- package/src/organisms/choice-picker/index.d.ts +1 -0
- package/src/organisms/choice-picker/index.js +1 -0
- package/src/organisms/combobox/combobox.css +40 -0
- package/src/organisms/combobox/combobox.js +309 -0
- package/src/organisms/combobox/index.js +5 -0
- package/src/organisms/index.d.ts +7 -0
- package/src/organisms/index.js +7 -0
- package/src/organisms/multi-select-field/index.d.ts +1 -0
- package/src/organisms/multi-select-field/index.js +1 -0
- package/src/organisms/multi-select-field/multi-select-field.css +76 -0
- package/src/organisms/multi-select-field/multi-select-field.d.ts +64 -0
- package/src/organisms/multi-select-field/multi-select-field.js +317 -0
- package/src/organisms/navigation-keys.js +58 -0
- package/src/organisms/segmented-control/index.d.ts +1 -0
- package/src/organisms/segmented-control/index.js +1 -0
- package/src/organisms/segmented-control/segmented-control.css +113 -0
- package/src/organisms/segmented-control/segmented-control.d.ts +43 -0
- package/src/organisms/segmented-control/segmented-control.js +163 -0
- package/src/organisms/selection-popup/index.js +1 -0
- package/src/organisms/selection-popup/selection-popup.css +90 -0
- package/src/organisms/selection-popup/selection-popup.js +100 -0
- package/src/organisms/tab-bar/index.d.ts +1 -0
- package/src/organisms/tab-bar/index.js +1 -0
- package/src/organisms/tab-bar/tab-bar.css +45 -0
- package/src/organisms/tab-bar/tab-bar.d.ts +37 -0
- package/src/organisms/tab-bar/tab-bar.js +104 -0
- package/src/organisms/tabs/index.d.ts +1 -0
- package/src/organisms/tabs/index.js +1 -0
- package/src/organisms/tabs/tabs.css +53 -0
- package/src/organisms/tabs/tabs.d.ts +48 -0
- package/src/organisms/tabs/tabs.js +152 -0
- package/styles.css +18 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token (MOLECULE) — a committed value. It delegates every bit of geometry to
|
|
3
|
+
* the Choice Chip owner, exactly as the Figma component does, and adds only the
|
|
4
|
+
* Mark axis. A token is content with an optional Remove button, not a control.
|
|
5
|
+
*/
|
|
6
|
+
import type { Ol8ChipSize } from '../choice-chip/choice-chip.js';
|
|
7
|
+
|
|
8
|
+
export type Ol8TokenSize = Ol8ChipSize;
|
|
9
|
+
/** Figma draws the last three; `auto` resolves to one of them. */
|
|
10
|
+
export type Ol8TokenMark = 'auto' | 'none' | 'check' | 'remove';
|
|
11
|
+
|
|
12
|
+
export declare const OL8_TOKEN_SIZES: readonly Ol8TokenSize[];
|
|
13
|
+
export declare const OL8_TOKEN_MARKS: readonly Ol8TokenMark[];
|
|
14
|
+
|
|
15
|
+
/** Remove for a removable value, Check for a selected one, None otherwise. */
|
|
16
|
+
export declare function resolveTokenMark(
|
|
17
|
+
mark: Ol8TokenMark,
|
|
18
|
+
state?: { removable?: boolean; selected?: boolean },
|
|
19
|
+
): Exclude<Ol8TokenMark, 'auto'>;
|
|
20
|
+
|
|
21
|
+
export interface Ol8TokenOptions {
|
|
22
|
+
size?: Ol8TokenSize;
|
|
23
|
+
selected?: boolean;
|
|
24
|
+
mark?: Ol8TokenMark;
|
|
25
|
+
/** A non removable token may not carry a Remove mark. */
|
|
26
|
+
removable?: boolean;
|
|
27
|
+
/** Overrides the default "Remove {label}" accessible name. */
|
|
28
|
+
removeLabel?: string;
|
|
29
|
+
/** False makes the Remove button reachable by arrow key rather than Tab. */
|
|
30
|
+
focusable?: boolean;
|
|
31
|
+
id?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export declare function renderToken(label: string, options?: Ol8TokenOptions): string;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Token (MOLECULE) — a committed value.
|
|
3
|
+
*
|
|
4
|
+
* Figma: Select and Combobox / Molecule / Token 897:2106 (24 variants,
|
|
5
|
+
* Size x Selection x Mark).
|
|
6
|
+
*
|
|
7
|
+
* From the Figma description:
|
|
8
|
+
* "Compact/Standard/Comfortable/Large delegate all 30/36/42/42px geometry to
|
|
9
|
+
* the Soft Hexagon Choice Chip owner. Selection and None/Check/Remove mark
|
|
10
|
+
* contracts remain independent; governed Check and Close atoms are reused
|
|
11
|
+
* instead of duplicated."
|
|
12
|
+
*
|
|
13
|
+
* So this draws no silhouette of its own. It instantiates the Choice Chip's
|
|
14
|
+
* geometry exactly as the Figma component instantiates the Geometry Owner, and
|
|
15
|
+
* adds the one thing that is genuinely its own: the Mark axis.
|
|
16
|
+
*
|
|
17
|
+
* A Token is not a Choice Chip. A chip is a checkbox a person toggles; a token
|
|
18
|
+
* is a value that has already been committed, so it is ordinary content with an
|
|
19
|
+
* optional Remove button rather than a form control.
|
|
20
|
+
*
|
|
21
|
+
* MARK
|
|
22
|
+
* Figma's axis is the concrete None / Check / Remove. The contract adds `auto`
|
|
23
|
+
* as the public default, which resolves rather than draws: "Remove for
|
|
24
|
+
* removable committed values, Check for selected Choice Chips, and None for
|
|
25
|
+
* unselected or informational chips." A variant cannot be `auto`, which is why
|
|
26
|
+
* Figma has no such option and code does.
|
|
27
|
+
*/
|
|
28
|
+
import { renderIconSvg } from '../../atoms/icon/icon.js';
|
|
29
|
+
import { renderChipShape, escapeAttr, OL8_CHIP_SIZES } from '../choice-chip/choice-chip.js';
|
|
30
|
+
|
|
31
|
+
export const OL8_TOKEN_SIZES = OL8_CHIP_SIZES;
|
|
32
|
+
/** Figma draws the last three. `auto` resolves to one of them. */
|
|
33
|
+
export const OL8_TOKEN_MARKS = ['auto', 'none', 'check', 'remove'];
|
|
34
|
+
|
|
35
|
+
/** The contract's resolution, stated once so both packages agree. */
|
|
36
|
+
export function resolveTokenMark(mark, { removable = true, selected = false } = {}) {
|
|
37
|
+
if (mark !== 'auto') return mark;
|
|
38
|
+
if (removable) return 'remove';
|
|
39
|
+
return selected ? 'check' : 'none';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} label
|
|
44
|
+
* @param {{size?:string,selected?:boolean,mark?:string,removable?:boolean,
|
|
45
|
+
* removeLabel?:string,id?:string,className?:string}} [options]
|
|
46
|
+
*/
|
|
47
|
+
export function renderToken(label, options = {}) {
|
|
48
|
+
const {
|
|
49
|
+
size = 'standard', selected = false, mark = 'auto', removable = true,
|
|
50
|
+
removeLabel, focusable = true, id, className,
|
|
51
|
+
} = options;
|
|
52
|
+
|
|
53
|
+
if (!OL8_TOKEN_SIZES.includes(size)) throw new Error(`[ol8] unknown token size "${size}"`);
|
|
54
|
+
if (!OL8_TOKEN_MARKS.includes(mark)) throw new Error(`[ol8] unknown token mark "${mark}"`);
|
|
55
|
+
if (!label || !String(label).trim()) throw new Error('[ol8] Token requires a label');
|
|
56
|
+
if (mark === 'remove' && !removable) {
|
|
57
|
+
throw new Error('[ol8] mark "remove" offers a removal path a non removable token does not have');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const uid = id ?? `ol8-token-${Math.random().toString(36).slice(2, 9)}`;
|
|
61
|
+
const resolved = resolveTokenMark(mark, { removable, selected });
|
|
62
|
+
|
|
63
|
+
// The Remove mark is the only interactive part, so it is a real button with
|
|
64
|
+
// its own accessible name. It follows the label, which is the anatomy the
|
|
65
|
+
// contract writes down, and the label stays the token's content: the complete
|
|
66
|
+
// value must remain readable, never shortened or overlaid by its mark.
|
|
67
|
+
const markup = resolved === 'remove'
|
|
68
|
+
? `<button type="button" class="ol8-chip__mark ol8-token__remove" ` +
|
|
69
|
+
(focusable ? '' : 'tabindex="-1" ') +
|
|
70
|
+
`aria-label="${escapeAttr(removeLabel ?? `Remove ${label}`)}">${renderIconSvg('close')}</button>`
|
|
71
|
+
: resolved === 'check'
|
|
72
|
+
? `<span class="ol8-chip__mark" aria-hidden="true">${renderIconSvg('check')}</span>`
|
|
73
|
+
: '';
|
|
74
|
+
|
|
75
|
+
const attrs = [
|
|
76
|
+
`class="ol8-token ol8-chip${className ? ` ${className}` : ''}"`,
|
|
77
|
+
`data-ol8-size="${size}"`,
|
|
78
|
+
`data-ol8-selection="${selected ? 'selected' : 'inactive'}"`,
|
|
79
|
+
`data-ol8-mark="${resolved}"`,
|
|
80
|
+
].join(' ');
|
|
81
|
+
|
|
82
|
+
return `<span ${attrs}>${renderChipShape(uid, label, markup)}</span>`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Choice Picker (ORGANISM)
|
|
3
|
+
* Figma: Organism / Choice Picker / Soft Hexagon 906:2898 (16 variants)
|
|
4
|
+
*
|
|
5
|
+
* Three parts stacked twelve apart: a search field, a wrapping matrix of chips,
|
|
6
|
+
* and in Apply mode a commitment line. The search field is the canonical Text
|
|
7
|
+
* Field owner and the chips are the canonical Choice Chip owner, so neither is
|
|
8
|
+
* restyled here.
|
|
9
|
+
*
|
|
10
|
+
* Read from all four sizes in Figma: the root gap, the matrix gap and the
|
|
11
|
+
* commitment typography do NOT scale with the picker size. They are twelve,
|
|
12
|
+
* six, and 12/18 at Compact, Standard, Comfortable and Large alike. Only the
|
|
13
|
+
* search field and the chips take the size.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
.ol8-picker {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: var(--ol8-spacing-stack-related);
|
|
20
|
+
inline-size: 100%;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* "The matrix wraps with 6px gaps and preserves source/reading order; it is not
|
|
24
|
+
a masonry layout." A plain wrapping flex row is exactly that. */
|
|
25
|
+
.ol8-picker__matrix {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
gap: var(--ol8-component-choice-chip-gap);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Figma: HORIZONTAL, space between, 24 tall, both texts secondary at 12/18. */
|
|
32
|
+
.ol8-picker__commitment {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: flex-start;
|
|
35
|
+
justify-content: space-between;
|
|
36
|
+
gap: var(--ol8-spacing-inline-standard);
|
|
37
|
+
min-block-size: var(--ol8-size-icon-standard);
|
|
38
|
+
font-size: var(--ol8-font-size-012);
|
|
39
|
+
line-height: var(--ol8-font-line-018);
|
|
40
|
+
color: var(--ol8-color-text-secondary);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.ol8-picker__summary {
|
|
44
|
+
font-family: var(--ol8-font-family-functional);
|
|
45
|
+
color: inherit;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Clear all and the apply hint are text weight actions rather than Button
|
|
49
|
+
instances. The contract asks for Clear all to be quiet, and Figma's row is
|
|
50
|
+
twenty four tall, which a Button's protected forty eight target would break.
|
|
51
|
+
They are still real buttons, so they are reachable and announced. */
|
|
52
|
+
.ol8-picker__apply,
|
|
53
|
+
.ol8-picker__clear {
|
|
54
|
+
padding: 0;
|
|
55
|
+
border: 0;
|
|
56
|
+
background: none;
|
|
57
|
+
font-family: var(--ol8-font-family-functional);
|
|
58
|
+
font-size: inherit;
|
|
59
|
+
line-height: inherit;
|
|
60
|
+
color: var(--ol8-color-actionquiet-contentdefault);
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
.ol8-picker__apply:hover,
|
|
64
|
+
.ol8-picker__clear:hover {
|
|
65
|
+
color: var(--ol8-color-actionquiet-contenthover);
|
|
66
|
+
text-decoration: underline;
|
|
67
|
+
}
|
|
68
|
+
.ol8-picker__apply:focus-visible,
|
|
69
|
+
.ol8-picker__clear:focus-visible {
|
|
70
|
+
outline: var(--ol8-focus-ring-innerwidth) solid var(--ol8-color-focus-inner);
|
|
71
|
+
outline-offset: var(--ol8-focus-ring-offset);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ol8-picker[data-ol8-availability="disabled"] .ol8-picker__apply,
|
|
75
|
+
.ol8-picker[data-ol8-availability="disabled"] .ol8-picker__clear {
|
|
76
|
+
color: var(--ol8-color-actionquiet-contentdisabled);
|
|
77
|
+
cursor: not-allowed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* The search label stays associated while the composition hides it. */
|
|
81
|
+
.ol8-picker__label-row--hidden {
|
|
82
|
+
position: absolute;
|
|
83
|
+
inline-size: 1px;
|
|
84
|
+
block-size: 1px;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
clip-path: inset(50%);
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Retrieval and result counts are announced rather than only drawn. */
|
|
91
|
+
.ol8-picker__status {
|
|
92
|
+
position: absolute;
|
|
93
|
+
inline-size: 1px;
|
|
94
|
+
block-size: 1px;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
clip-path: inset(50%);
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choice Picker (ORGANISM) — search over a wrapping matrix of choices.
|
|
3
|
+
* The inline composition is canonical: an accessible group holding a search
|
|
4
|
+
* field, real checkboxes wearing the Choice Chip appearance, and, in Apply
|
|
5
|
+
* mode, a commitment line. It is deliberately not a dialog pretending to be a
|
|
6
|
+
* listbox, because the surface holds search, many choices and actions.
|
|
7
|
+
*/
|
|
8
|
+
export type Ol8PickerSize = 'compact' | 'standard' | 'comfortable' | 'large';
|
|
9
|
+
export type Ol8PickerMaterial = 'regular' | 'gem';
|
|
10
|
+
export type Ol8CommitBehavior = 'immediate' | 'apply';
|
|
11
|
+
export type Ol8PickerPresentation = 'inline' | 'popup';
|
|
12
|
+
/** One treatment across an instance; peers never mix Check and None. */
|
|
13
|
+
export type Ol8PickerMark = 'check' | 'none';
|
|
14
|
+
|
|
15
|
+
export declare const OL8_PICKER_SIZES: readonly Ol8PickerSize[];
|
|
16
|
+
export declare const OL8_PICKER_MATERIALS: readonly Ol8PickerMaterial[];
|
|
17
|
+
export declare const OL8_COMMIT_BEHAVIORS: readonly Ol8CommitBehavior[];
|
|
18
|
+
export declare const OL8_PICKER_PRESENTATIONS: readonly Ol8PickerPresentation[];
|
|
19
|
+
export declare const OL8_PICKER_MARKS: readonly Ol8PickerMark[];
|
|
20
|
+
|
|
21
|
+
export interface Ol8PickerOption {
|
|
22
|
+
value: string;
|
|
23
|
+
label: string;
|
|
24
|
+
selected?: boolean;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Ol8ChoicePickerOptions {
|
|
29
|
+
/** Names the search. Kept associated even when visually hidden. */
|
|
30
|
+
label: string;
|
|
31
|
+
id?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
options?: Ol8PickerOption[];
|
|
34
|
+
/** Apply holds a pending set; immediate commits each toggle at once. */
|
|
35
|
+
commitBehavior?: Ol8CommitBehavior;
|
|
36
|
+
presentation?: Ol8PickerPresentation;
|
|
37
|
+
mark?: Ol8PickerMark;
|
|
38
|
+
size?: Ol8PickerSize;
|
|
39
|
+
material?: Ol8PickerMaterial;
|
|
40
|
+
inputValue?: string;
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
showLabel?: boolean;
|
|
43
|
+
selectedSummary?: string;
|
|
44
|
+
/** Apply mode only. Immediate mode rejects it rather than drawing a lie. */
|
|
45
|
+
applyHint?: string;
|
|
46
|
+
showClearAll?: boolean;
|
|
47
|
+
clearAllLabel?: string;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
readOnly?: boolean;
|
|
50
|
+
status?: 'none' | 'loading' | 'empty' | 'error';
|
|
51
|
+
statusText?: string;
|
|
52
|
+
className?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export declare function renderChoicePicker(options: Ol8ChoicePickerOptions): string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply mode only. Enter commits the pending set, Escape restores the last
|
|
59
|
+
* committed one without closing or moving focus, and a composing IME is left
|
|
60
|
+
* alone. Both dispatch events rather than mutating state.
|
|
61
|
+
*/
|
|
62
|
+
export declare function hydrateChoicePickers(root?: ParentNode): number;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Choice Picker (ORGANISM)
|
|
3
|
+
*
|
|
4
|
+
* Figma: Organism / Choice Picker / Soft Hexagon 906:2898 (16 variants,
|
|
5
|
+
* Material x Commitment x Size).
|
|
6
|
+
*
|
|
7
|
+
* From the Figma description:
|
|
8
|
+
* "Choice Picker organism using canonical search-field and Soft Hexagon
|
|
9
|
+
* Choice Chip owners. Size propagates through all children; Commitment
|
|
10
|
+
* controls Apply versus Immediate behavior."
|
|
11
|
+
*
|
|
12
|
+
* And from the maintainer decision recorded on the same component:
|
|
13
|
+
* "Public ChoicePicker defaults to presentation=inline ... Search, existing
|
|
14
|
+
* Choice Chip/Token molecules and commitment summary remain in place. Apply
|
|
15
|
+
* mode holds pending selection; Enter from Search or a checkbox commits it,
|
|
16
|
+
* Escape restores the last committed set without closing the inline panel.
|
|
17
|
+
* The Enter-to-apply hint is also a focusable clickable action ... Immediate
|
|
18
|
+
* mode commits each toggle and omits that action."
|
|
19
|
+
*
|
|
20
|
+
* So the inline composition is canonical, and it is a group rather than a
|
|
21
|
+
* dialog: a search field, a wrapping matrix of real checkboxes wearing the
|
|
22
|
+
* chip appearance, and a commitment line that exists only in Apply mode.
|
|
23
|
+
*
|
|
24
|
+
* THE HINT IS A BUTTON, NOT A SENTENCE
|
|
25
|
+
* Figma draws "Enter to apply" as text. The decision above requires it to be an
|
|
26
|
+
* actual clickable and focusable action, because a keyboard instruction is not
|
|
27
|
+
* an affordance for anyone using a pointer, touch, or a screen reader. So it is
|
|
28
|
+
* a button that reads as the hint.
|
|
29
|
+
*/
|
|
30
|
+
import { renderChoiceChip } from '../../molecules/choice-chip/choice-chip.js';
|
|
31
|
+
import { renderIcon } from '../../atoms/icon/icon.js';
|
|
32
|
+
|
|
33
|
+
export const OL8_PICKER_SIZES = ['compact', 'standard', 'comfortable', 'large'];
|
|
34
|
+
export const OL8_PICKER_MATERIALS = ['regular', 'gem'];
|
|
35
|
+
export const OL8_COMMIT_BEHAVIORS = ['immediate', 'apply'];
|
|
36
|
+
export const OL8_PICKER_PRESENTATIONS = ['inline', 'popup'];
|
|
37
|
+
/** One mark treatment across an instance; peers never mix Check and None. */
|
|
38
|
+
export const OL8_PICKER_MARKS = ['check', 'none'];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {{
|
|
42
|
+
* label:string, id?:string, name?:string,
|
|
43
|
+
* options?:Array<{value:string,label:string,selected?:boolean,disabled?:boolean}>,
|
|
44
|
+
* commitBehavior?:string, presentation?:string, mark?:string,
|
|
45
|
+
* size?:string, material?:string,
|
|
46
|
+
* inputValue?:string, placeholder?:string, showLabel?:boolean,
|
|
47
|
+
* selectedSummary?:string, applyHint?:string,
|
|
48
|
+
* showClearAll?:boolean, clearAllLabel?:string,
|
|
49
|
+
* disabled?:boolean, readOnly?:boolean,
|
|
50
|
+
* status?:string, statusText?:string, className?:string,
|
|
51
|
+
* }} options
|
|
52
|
+
*/
|
|
53
|
+
export function renderChoicePicker(options = {}) {
|
|
54
|
+
const {
|
|
55
|
+
label, id, name, options: items = [],
|
|
56
|
+
commitBehavior = 'immediate', presentation = 'inline', mark = 'check',
|
|
57
|
+
size = 'comfortable', material = 'regular',
|
|
58
|
+
inputValue = '', placeholder, showLabel = false,
|
|
59
|
+
selectedSummary, applyHint = 'Enter to apply',
|
|
60
|
+
showClearAll = false, clearAllLabel = 'Clear all',
|
|
61
|
+
disabled = false, readOnly = false,
|
|
62
|
+
status = 'none', statusText, className,
|
|
63
|
+
} = options;
|
|
64
|
+
|
|
65
|
+
if (!label || !String(label).trim()) throw new Error('[ol8] a Choice Picker requires a label for its search');
|
|
66
|
+
if (!OL8_PICKER_SIZES.includes(size)) throw new Error(`[ol8] unknown size "${size}"`);
|
|
67
|
+
if (!OL8_PICKER_MATERIALS.includes(material)) throw new Error(`[ol8] unknown material "${material}"`);
|
|
68
|
+
if (!OL8_COMMIT_BEHAVIORS.includes(commitBehavior)) throw new Error(`[ol8] unknown commit behavior "${commitBehavior}"`);
|
|
69
|
+
if (!OL8_PICKER_PRESENTATIONS.includes(presentation)) throw new Error(`[ol8] unknown presentation "${presentation}"`);
|
|
70
|
+
if (!OL8_PICKER_MARKS.includes(mark)) throw new Error(`[ol8] unknown mark "${mark}"`);
|
|
71
|
+
if (commitBehavior === 'immediate' && options.applyHint !== undefined) {
|
|
72
|
+
throw new Error('[ol8] immediate commitment applies every toggle at once, so an apply action would be a lie');
|
|
73
|
+
}
|
|
74
|
+
if (status !== 'none' && !statusText) {
|
|
75
|
+
throw new Error('[ol8] a picker status needs words. A spinner alone tells a screen reader nothing.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const uid = id ?? `ol8-picker-${Math.random().toString(36).slice(2, 9)}`;
|
|
79
|
+
const labelId = `${uid}-label`;
|
|
80
|
+
const statusId = `${uid}-status`;
|
|
81
|
+
const selected = items.filter(o => o.selected);
|
|
82
|
+
|
|
83
|
+
// Every option is a real checkbox wearing the chip appearance. The mark
|
|
84
|
+
// treatment is the same for every peer, which is why it is one setting on
|
|
85
|
+
// the picker rather than a property of each chip.
|
|
86
|
+
const matrix = items.map((o, i) => renderChoiceChip(o.label, {
|
|
87
|
+
size,
|
|
88
|
+
selected: !!o.selected,
|
|
89
|
+
showMark: mark === 'check' && !!o.selected,
|
|
90
|
+
disabled: disabled || readOnly || o.disabled,
|
|
91
|
+
name: name ? `${name}[]` : undefined,
|
|
92
|
+
value: o.value,
|
|
93
|
+
id: `${uid}-option-${i}`,
|
|
94
|
+
className: 'ol8-picker__chip',
|
|
95
|
+
})).join('');
|
|
96
|
+
|
|
97
|
+
const summary = selectedSummary
|
|
98
|
+
?? `${selected.length} selected`;
|
|
99
|
+
|
|
100
|
+
// Apply mode holds a pending set, so it says what is pending and offers the
|
|
101
|
+
// way to commit it. Immediate mode has nothing pending and says nothing.
|
|
102
|
+
const commitment = commitBehavior === 'apply'
|
|
103
|
+
? `<div class="ol8-picker__commitment">` +
|
|
104
|
+
`<span class="ol8-picker__summary">${escapeText(summary)}</span>` +
|
|
105
|
+
(showClearAll
|
|
106
|
+
? `<button type="button" class="ol8-picker__clear">${escapeText(clearAllLabel)}</button>`
|
|
107
|
+
: '') +
|
|
108
|
+
`<button type="button" class="ol8-picker__apply">${escapeText(applyHint)}</button>` +
|
|
109
|
+
`</div>`
|
|
110
|
+
: '';
|
|
111
|
+
|
|
112
|
+
const search =
|
|
113
|
+
`<div class="ol8-field ol8-picker__field" data-ol8-size="${size}" data-ol8-appearance="outline"` +
|
|
114
|
+
(material === 'gem' ? ' data-ol8-material="gem"' : '') + '>' +
|
|
115
|
+
`<div class="ol8-field__label-row${showLabel ? '' : ' ol8-picker__label-row--hidden'}">` +
|
|
116
|
+
`<label class="ol8-field__label" id="${labelId}" for="${uid}">${escapeText(label)}</label>` +
|
|
117
|
+
`</div>` +
|
|
118
|
+
`<div class="ol8-field__control-stack"><div class="ol8-field__control">` +
|
|
119
|
+
renderIcon('search', { size: 18, className: 'ol8-field__leading-icon' }) +
|
|
120
|
+
`<input class="ol8-field__input ol8-picker__input" id="${uid}" type="search"` +
|
|
121
|
+
` value="${escapeAttr(inputValue)}"` +
|
|
122
|
+
(placeholder ? ` placeholder="${escapeAttr(placeholder)}"` : '') +
|
|
123
|
+
(disabled ? ' disabled' : '') + (readOnly ? ' readonly' : '') +
|
|
124
|
+
` aria-describedby="${statusId}">` +
|
|
125
|
+
`</div></div>` +
|
|
126
|
+
`</div>`;
|
|
127
|
+
|
|
128
|
+
const rootAttrs = [
|
|
129
|
+
`class="ol8-picker${className ? ` ${className}` : ''}"`,
|
|
130
|
+
`data-ol8-size="${size}"`,
|
|
131
|
+
`data-ol8-commitment="${commitBehavior}"`,
|
|
132
|
+
`data-ol8-presentation="${presentation}"`,
|
|
133
|
+
`data-ol8-mark="${mark}"`,
|
|
134
|
+
material === 'gem' ? 'data-ol8-material="gem"' : '',
|
|
135
|
+
disabled ? 'data-ol8-availability="disabled"' : '',
|
|
136
|
+
readOnly ? 'data-ol8-readonly="true"' : '',
|
|
137
|
+
// A group, not a dialog: the surface holds search, many independent
|
|
138
|
+
// choices and actions, so pretending it is one listbox would be a lie.
|
|
139
|
+
`role="group" aria-labelledby="${labelId}"`,
|
|
140
|
+
].filter(Boolean).join(' ');
|
|
141
|
+
|
|
142
|
+
return `<div ${rootAttrs}>` +
|
|
143
|
+
search +
|
|
144
|
+
`<div class="ol8-picker__matrix">${matrix}</div>` +
|
|
145
|
+
commitment +
|
|
146
|
+
`<span class="ol8-picker__status" id="${statusId}" role="status" aria-live="polite">` +
|
|
147
|
+
escapeText(status !== 'none' ? statusText : '') +
|
|
148
|
+
`</span>` +
|
|
149
|
+
`</div>`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Binds every picker under `root`. Idempotent.
|
|
154
|
+
*
|
|
155
|
+
* Only Apply mode needs behaviour: it holds a pending set, so Enter commits it
|
|
156
|
+
* and Escape restores the last committed one without closing the panel or
|
|
157
|
+
* moving focus. Immediate mode has nothing to hold, so a toggle is the whole
|
|
158
|
+
* interaction and the browser already does it.
|
|
159
|
+
*
|
|
160
|
+
* Both dispatch events rather than mutating state, because the selection
|
|
161
|
+
* belongs to the application.
|
|
162
|
+
*/
|
|
163
|
+
export function hydrateChoicePickers(root = document) {
|
|
164
|
+
const pickers = [...root.querySelectorAll('.ol8-picker')];
|
|
165
|
+
|
|
166
|
+
for (const picker of pickers) {
|
|
167
|
+
if (picker.dataset.ol8PickerBound === 'true') continue;
|
|
168
|
+
if (picker.dataset.ol8Commitment !== 'apply') { picker.dataset.ol8PickerBound = 'true'; continue; }
|
|
169
|
+
|
|
170
|
+
const commit = () => picker.dispatchEvent(new CustomEvent('ol8:apply', { bubbles: true }));
|
|
171
|
+
const restore = () => picker.dispatchEvent(new CustomEvent('ol8:cancel', { bubbles: true }));
|
|
172
|
+
|
|
173
|
+
picker.addEventListener('keydown', (event) => {
|
|
174
|
+
// A composing IME uses Enter to choose a candidate. Committing there
|
|
175
|
+
// would apply a selection the person was in the middle of typing.
|
|
176
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
177
|
+
const from = event.target;
|
|
178
|
+
const inSearch = from.classList && from.classList.contains('ol8-picker__input');
|
|
179
|
+
const inChip = from.closest && from.closest('.ol8-picker__chip');
|
|
180
|
+
if (event.key === 'Enter' && (inSearch || inChip)) {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
commit();
|
|
183
|
+
} else if (event.key === 'Escape') {
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
restore(); // focus deliberately stays put
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const apply = picker.querySelector(':scope .ol8-picker__apply');
|
|
190
|
+
if (apply) apply.addEventListener('click', commit);
|
|
191
|
+
const clear = picker.querySelector(':scope .ol8-picker__clear');
|
|
192
|
+
if (clear) clear.addEventListener('click', () =>
|
|
193
|
+
picker.dispatchEvent(new CustomEvent('ol8:clearall', { bubbles: true })));
|
|
194
|
+
|
|
195
|
+
picker.dataset.ol8PickerBound = 'true';
|
|
196
|
+
}
|
|
197
|
+
return pickers.length;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function escapeAttr(v) { return String(v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
|
201
|
+
function escapeText(v) { return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './choice-picker.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './choice-picker.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Combobox (ORGANISM)
|
|
3
|
+
* Figma: Organism / Combobox 883:2159 — "Reuses approved Text Field, Selection
|
|
4
|
+
* Popup and governed icon atoms."
|
|
5
|
+
*
|
|
6
|
+
* So the field geometry is the field's, the popup is the popup's, and this
|
|
7
|
+
* file adds only what belongs to the combination: where the popup sits, and
|
|
8
|
+
* the two marks at the trailing edge.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.ol8-combobox { position: relative; }
|
|
12
|
+
|
|
13
|
+
/* "Closed control to popup block gap" is a token, so the popup sits where the
|
|
14
|
+
design says rather than wherever absolute positioning lands it. */
|
|
15
|
+
.ol8-combobox__popup {
|
|
16
|
+
position: absolute;
|
|
17
|
+
z-index: 1;
|
|
18
|
+
inset-inline: 0;
|
|
19
|
+
inset-block-start: calc(100% + var(--ol8-component-selectionpopup-gap));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.ol8-combobox__disclosure {
|
|
23
|
+
flex: none;
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
color: var(--ol8-color-icon-secondary);
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.ol8-combobox__clear {
|
|
30
|
+
flex: none;
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
border: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
background: none;
|
|
35
|
+
color: var(--ol8-color-icon-secondary);
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
}
|
|
38
|
+
.ol8-combobox__clear:disabled { color: var(--ol8-color-icon-disabled); cursor: not-allowed; }
|
|
39
|
+
|
|
40
|
+
.ol8-combobox[data-ol8-disabled] .ol8-combobox__disclosure { color: var(--ol8-color-icon-disabled); cursor: not-allowed; }
|