@repobit/dex-system-design 0.25.12 → 0.26.0
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/CHANGELOG.md +187 -0
- package/README.md +131 -8
- package/package.json +95 -13
- package/schemas/component-manifest.json +3294 -405
- package/schemas/manifest-omissions.json +173 -0
- package/src/blog/src/breakpoints.js +6 -6
- package/src/blog/src/content/blog-atoms-showcase.js +1 -1
- package/src/blog/src/content/blog-page-layout.js +2 -2
- package/src/blog/src/lib/article-toc.js +7 -7
- package/src/blog/src/shell/blog-shell.js +1 -1
- package/src/blog/src/tokens/blog.js +2 -2
- package/src/blog/src/tokens/core.js +34 -35
- package/src/blog/src/tokens/legacy.js +0 -9
- package/src/blog/tokens/blog.js +2 -2
- package/src/blog/tokens/core.js +34 -35
- package/src/blog/tokens/legacy.js +0 -9
- package/src/blog/tokens.css +36 -46
- package/src/components/accordion/accordion-bg.css.js +83 -59
- package/src/components/accordion/accordion-bg.js +29 -10
- package/src/components/accordion/accordion-bg.stories.js +67 -67
- package/src/components/accordion/accordion-keyboard.js +80 -0
- package/src/components/accordion/accordion.css.js +142 -80
- package/src/components/accordion/accordion.js +37 -10
- package/src/components/accordion/accordion.stories.js +7 -7
- package/src/components/agent-endpoints/agent-endpoints.css.js +36 -39
- package/src/components/agent-endpoints/agent-endpoints.js +30 -20
- package/src/components/agent-endpoints/agent-endpoints.stories.js +1 -1
- package/src/components/ai-guardian-demo.stories.js +91 -91
- package/src/components/alert/alert.css.js +198 -0
- package/src/components/alert/alert.js +103 -0
- package/src/components/alert/alert.stories.js +206 -0
- package/src/components/anchor/anchor-nav.css.js +213 -101
- package/src/components/anchor/anchor-nav.js +26 -40
- package/src/components/anchor/anchor.stories.js +22 -12
- package/src/components/annotation/annotation.css.js +58 -0
- package/src/components/annotation/annotation.js +67 -0
- package/src/components/annotation/annotation.stories.js +63 -0
- package/src/components/architecture.stories.js +285 -0
- package/src/components/atoms.stories.js +342 -0
- package/src/components/avatar/avatar.css.js +67 -0
- package/src/components/avatar/avatar.js +42 -0
- package/src/components/avatar/avatar.stories.js +95 -0
- package/src/components/awards/awards-flex.css.js +209 -23
- package/src/components/awards/awards-flex.js +66 -7
- package/src/components/awards/awards-flex.stories.js +1 -1
- package/src/components/awards/{awards-light.stories.js → awards-summary.stories.js} +54 -64
- package/src/components/awards/awards.css.js +116 -58
- package/src/components/awards/awards.js +102 -28
- package/src/components/awards/awards.stories.js +1 -1
- package/src/components/back/back.css.js +51 -22
- package/src/components/back/back.js +28 -7
- package/src/components/back/back.stories.js +9 -3
- package/src/components/badge/badge.css.js +158 -46
- package/src/components/badge/badge.js +62 -74
- package/src/components/badge/badge.stories.js +176 -248
- package/src/components/bento-grid/bento-grid.css.js +21 -16
- package/src/components/bento-grid/bento-grid.js +16 -4
- package/src/components/bento-grid/bento-grid.stories.js +19 -18
- package/src/components/bento-grid/bento-item.css.js +52 -28
- package/src/components/bento-grid/bento-item.js +2 -2
- package/src/components/blockquote/blockquote.css.js +47 -0
- package/src/components/blockquote/blockquote.js +29 -0
- package/src/components/blockquote/blockquote.stories.js +62 -0
- package/src/components/breadcrumb/breadcrumb.css.js +92 -22
- package/src/components/breadcrumb/breadcrumb.js +56 -11
- package/src/components/button/button.css.js +531 -161
- package/src/components/button/button.js +206 -214
- package/src/components/button/button.stories.js +322 -174
- package/src/components/caption/caption.css.js +21 -0
- package/src/components/caption/caption.js +31 -0
- package/src/components/caption/caption.stories.js +62 -0
- package/src/components/card-features/card-features.css.js +14 -22
- package/src/components/card-features/card-features.js +68 -9
- package/src/components/card-features/card-features.stories.js +11 -11
- package/src/components/cards/card.css.js +60 -81
- package/src/components/cards/card.js +56 -25
- package/src/components/cards/card.stories.js +39 -39
- package/src/components/carousel/carousel.css.js +93 -99
- package/src/components/carousel/carousel.js +32 -4
- package/src/components/checkbox/checkbox.css.js +111 -93
- package/src/components/checkbox/checkbox.js +64 -90
- package/src/components/checkbox/checkbox.stories.js +96 -29
- package/src/components/code/code.css.js +70 -0
- package/src/components/code/code.js +40 -0
- package/src/components/code/code.stories.js +73 -0
- package/src/components/compare/compare.css.js +179 -43
- package/src/components/compare/compare.js +52 -25
- package/src/components/compare/compare.stories.js +61 -4
- package/src/components/comparison-panel/comparison-panel.css.js +38 -51
- package/src/components/comparison-panel/comparison-panel.js +5 -4
- package/src/components/comparison-panel/comparison-panel.stories.js +1 -1
- package/src/components/compatibility-table/compatibility-table.css.js +180 -80
- package/src/components/compatibility-table/compatibility-table.js +37 -9
- package/src/components/control-alignment.stories.js +393 -0
- package/src/components/cta-banner/cta-banner.css.js +41 -13
- package/src/components/cta-banner/cta-banner.js +6 -5
- package/src/components/cta-banner/cta-banner.stories.js +43 -98
- package/src/components/display/display.css.js +25 -1
- package/src/components/display/display.js +46 -13
- package/src/components/display/display.stories.js +270 -40
- package/src/components/divider/divider-horizontal.js +21 -5
- package/src/components/divider/divider-vertical.js +15 -7
- package/src/components/divider/divider.css.js +2 -2
- package/src/components/divider/divider.stories.js +38 -34
- package/src/components/dropdown/dropdown.css.js +93 -126
- package/src/components/dropdown/dropdown.js +114 -34
- package/src/components/dropdown/dropdown.stories.js +56 -24
- package/src/components/features/features.css.js +24 -11
- package/src/components/features/features.js +7 -3
- package/src/components/features/features.stories.js +37 -37
- package/src/components/features-tab/features-tab.css.js +19 -8
- package/src/components/features-tab/features-tab.js +6 -5
- package/src/components/features-tab/features-tab.stories.js +23 -23
- package/src/components/fieldset/fieldset.css.js +30 -0
- package/src/components/fieldset/fieldset.js +28 -0
- package/src/components/fieldset/fieldset.stories.js +84 -0
- package/src/components/footer/footer-links-group.css.js +10 -14
- package/src/components/footer/footer-links-group.js +4 -3
- package/src/components/footer/footer-lp.css.js +59 -30
- package/src/components/footer/footer-lp.js +17 -12
- package/src/components/footer/footer-lp.stories.js +63 -75
- package/src/components/footer/footer-nav-menu.css.js +26 -4
- package/src/components/footer/footer-nav-menu.js +3 -2
- package/src/components/footer/footer.css.js +146 -264
- package/src/components/footer/footer.js +176 -106
- package/src/components/footer/footer.stories.js +9 -9
- package/src/components/footer/localeMap.js +1 -1
- package/src/components/grid/grid.css.js +1 -1
- package/src/components/grid/grid.js +49 -7
- package/src/components/grid/grid.stories.js +111 -0
- package/src/components/header/header-code.css.js +61 -51
- package/src/components/header/header-code.js +28 -14
- package/src/components/header/header-code.stories.js +5 -5
- package/src/components/header/header.css.js +655 -176
- package/src/components/header/header.js +269 -36
- package/src/components/header/header.stories.js +841 -18
- package/src/components/heading/heading.css.js +14 -17
- package/src/components/heading/heading.js +48 -33
- package/src/components/heading/heading.stories.js +27 -25
- package/src/components/help-text/help-text.css.js +21 -0
- package/src/components/help-text/help-text.js +25 -0
- package/src/components/help-text/help-text.stories.js +66 -0
- package/src/components/highlight/highlight.css.js +86 -64
- package/src/components/highlight/highlight.js +18 -27
- package/src/components/highlight/highlight.stories.js +14 -3
- package/src/components/icons/analysis-icon.js +6 -16
- package/src/components/icons/arrow-down-icon.js +6 -19
- package/src/components/icons/arrow-icon.js +44 -0
- package/src/components/icons/arrow-right-icon.js +30 -23
- package/src/components/icons/arrow-up-icon.js +19 -18
- package/src/components/icons/bd-ai-icon.js +1 -1
- package/src/components/icons/brand-marks.stories.js +279 -0
- package/src/components/icons/brand-signature-icon.js +42 -0
- package/src/components/icons/check-icon.js +27 -0
- package/src/components/icons/circle-info-icon.js +33 -0
- package/src/components/icons/close-icon.js +6 -18
- package/src/components/icons/family-icon.js +8 -20
- package/src/components/icons/globe-icon.js +6 -18
- package/src/components/icons/guide-icon.js +6 -18
- package/src/components/icons/icon-element.js +99 -0
- package/src/components/icons/icons.js +36 -0
- package/src/components/icons/icons.stories.js +184 -52
- package/src/components/icons/individual-icon.js +6 -18
- package/src/components/icons/laurel-icon.js +9 -19
- package/src/components/icons/machine-learning-icon.js +6 -17
- package/src/components/icons/marks.js +41 -0
- package/src/components/icons/minus-icon.js +7 -21
- package/src/components/icons/missing-icon.js +52 -0
- package/src/components/icons/plus-icon.js +7 -21
- package/src/components/icons/quote-icon.js +19 -19
- package/src/components/image/image.css.js +72 -25
- package/src/components/image/image.js +11 -6
- package/src/components/image/image.stories.js +2 -2
- package/src/components/input/custom-form.stories.js +214 -62
- package/src/components/input/input-clipboard.css.js +51 -144
- package/src/components/input/input-clipboard.js +31 -15
- package/src/components/input/input.css.js +8 -146
- package/src/components/input/input.js +93 -38
- package/src/components/install-options/install-options.css.js +84 -77
- package/src/components/install-options/install-options.js +33 -30
- package/src/components/install-options/install-options.stories.js +3 -3
- package/src/components/label/label.css.js +35 -0
- package/src/components/label/label.js +38 -0
- package/src/components/label/label.stories.js +82 -0
- package/src/components/light-carousel/light-carousel.css.js +111 -63
- package/src/components/light-carousel/light-carousel.js +85 -11
- package/src/components/light-carousel/light-carousel.stories.js +126 -82
- package/src/components/link/link.css.js +150 -99
- package/src/components/link/link.js +86 -11
- package/src/components/link/link.stories.js +124 -30
- package/src/components/list/list-item/list-item.css.js +74 -22
- package/src/components/list/list-item/list-item.js +32 -10
- package/src/components/list/list-item/list-item.stories.js +23 -23
- package/src/components/list/list.css.js +8 -2
- package/src/components/list/list.js +3 -2
- package/src/components/list/list.stories.js +222 -0
- package/src/components/modal/modal.css.js +114 -24
- package/src/components/modal/modal.js +83 -7
- package/src/components/modal/modal.stories.js +9 -9
- package/src/components/overview/overview.css.js +19 -14
- package/src/components/overview/overview.js +6 -5
- package/src/components/overview/overview.stories.js +10 -10
- package/src/components/page-switcher/page-switcher.css.js +97 -40
- package/src/components/page-switcher/page-switcher.js +28 -5
- package/src/components/page-switcher/page-switcher.stories.js +95 -0
- package/src/components/paragraph/paragraph.css.js +15 -10
- package/src/components/paragraph/paragraph.js +41 -44
- package/src/components/paragraph/paragraph.stories.js +56 -21
- package/src/components/picture/picture.css.js +103 -44
- package/src/components/picture/picture.js +100 -30
- package/src/components/picture/picture.stories.js +1 -1
- package/src/components/pricing-cards/new-pricing-card.js +3 -3
- package/src/components/pricing-cards/new-pricing.css.js +24 -15
- package/src/components/pricing-cards/pricing-card-actions.css.js +1 -1
- package/src/components/pricing-cards/pricing-card-actions.js +3 -4
- package/src/components/pricing-cards/pricing-card-container.css.js +3 -3
- package/src/components/pricing-cards/pricing-card-container.js +3 -2
- package/src/components/pricing-cards/pricing-card-header.css.js +10 -8
- package/src/components/pricing-cards/pricing-card-header.js +6 -7
- package/src/components/pricing-cards/pricing-card-pricing.css.js +16 -9
- package/src/components/pricing-cards/pricing-card-pricing.js +8 -6
- package/src/components/pricing-cards/pricing-card-show-more.css.js +11 -3
- package/src/components/pricing-cards/pricing-card-show-more.js +8 -3
- package/src/components/pricing-cards/pricing-card.css.js +244 -190
- package/src/components/pricing-cards/pricing-card.js +54 -70
- package/src/components/pricing-cards/pricing-feature-item.css.js +25 -8
- package/src/components/pricing-cards/pricing-feature-item.js +8 -3
- package/src/components/process-steps/process-steps.css.js +26 -14
- package/src/components/process-steps/process-steps.js +23 -6
- package/src/components/process-steps/process-steps.stories.js +1 -1
- package/src/components/progress/progress.css.js +80 -0
- package/src/components/progress/progress.js +42 -0
- package/src/components/progress/progress.stories.js +79 -0
- package/src/components/quick-answer/quick-answer.css.js +75 -13
- package/src/components/quick-answer/quick-answer.js +8 -3
- package/src/components/radio/radio.css.js +92 -128
- package/src/components/radio/radio.js +106 -127
- package/src/components/radio/radio.stories.js +54 -19
- package/src/components/review/review-grid.css.js +5 -6
- package/src/components/review/review-grid.js +14 -11
- package/src/components/review/review.css.js +11 -6
- package/src/components/review/review.js +3 -8
- package/src/components/shared/ensemble.css.js +93 -0
- package/src/components/shared/form-field.css.js +206 -0
- package/src/components/shared/prop-vocabulary.js +48 -0
- package/src/components/shared/token-scales.js +306 -0
- package/src/components/skeleton/skeleton.css.js +34 -0
- package/src/components/skeleton/skeleton.js +32 -0
- package/src/components/skeleton/skeleton.stories.js +70 -0
- package/src/components/spinner/spinner.css.js +62 -0
- package/src/components/spinner/spinner.js +38 -0
- package/src/components/spinner/spinner.stories.js +92 -0
- package/src/components/spotlight-section/spotlight-section.css.js +14 -13
- package/src/components/spotlight-section/spotlight-section.js +42 -39
- package/src/components/switch/switch.css.js +103 -0
- package/src/components/switch/switch.js +44 -0
- package/src/components/switch/switch.stories.js +86 -0
- package/src/components/tab-list/content-switcher.stories.js +189 -0
- package/src/components/tab-list/tab-list.css.js +250 -0
- package/src/components/tab-list/tab-list.js +184 -0
- package/src/components/tab-list/tab-list.stories.js +221 -0
- package/src/components/tabs/tabs.css.js +14 -123
- package/src/components/tabs/tabs.js +16 -19
- package/src/components/tabs/tabs.stories.js +95 -95
- package/src/components/tag/tag.css.js +54 -0
- package/src/components/tag/tag.js +32 -0
- package/src/components/tag/tag.stories.js +126 -0
- package/src/components/terminal/terminal-message.css.js +164 -60
- package/src/components/terminal/terminal-message.js +5 -5
- package/src/components/terminal/terminal-message.stories.js +70 -1
- package/src/components/termsOfUse/terms.css.js +25 -20
- package/src/components/termsOfUse/terms.js +13 -9
- package/src/components/termsOfUse/terms.stories.js +1 -1
- package/src/components/textarea/textarea.css.js +51 -0
- package/src/components/textarea/textarea.js +136 -0
- package/src/components/textarea/textarea.stories.js +191 -0
- package/src/components/tooltip/tooltip.css.js +75 -0
- package/src/components/tooltip/tooltip.js +156 -0
- package/src/components/tooltip/tooltip.stories.js +161 -0
- package/src/components/trust-panel/trust-panel.css.js +85 -33
- package/src/components/trust-panel/trust-panel.js +18 -10
- package/src/components/trust-panel/trust-panel.stories.js +1 -1
- package/src/components/ultimate-security-demo.stories.js +431 -0
- package/src/components/video/video.css.js +17 -0
- package/src/components/video/video.js +40 -0
- package/src/components/video/video.stories.js +106 -0
- package/src/components/video-player/video-player.css.js +67 -11
- package/src/components/video-player/video-player.js +35 -4
- package/src/components/video-player/video-player.stories.js +1 -1
- package/src/components/visual-alignment.stories.js +411 -0
- package/src/components/visually-hidden/visually-hidden.css.js +24 -0
- package/src/components/visually-hidden/visually-hidden.js +16 -0
- package/src/components/visually-hidden/visually-hidden.stories.js +56 -0
- package/src/stories/Button.stories.js +17 -17
- package/src/stories/Header.stories.js +10 -10
- package/src/stories/Page.js +1 -1
- package/src/stories/Page.stories.js +6 -6
- package/src/tokens/borders.stories.js +339 -0
- package/src/tokens/color-responsibilities.stories.js +227 -0
- package/src/tokens/dark-derivation.js +309 -0
- package/src/tokens/dark-findings.js +197 -0
- package/src/tokens/dark-mode.stories.js +207 -0
- package/src/tokens/dimension.stories.js +251 -0
- package/src/tokens/elevation.stories.js +143 -0
- package/src/tokens/focus.stories.js +276 -0
- package/src/tokens/fonts.css +20 -20
- package/src/tokens/fonts.stories.js +354 -374
- package/src/tokens/layout.css +25 -11
- package/src/tokens/motion.stories.js +231 -0
- package/src/tokens/shape.stories.js +272 -0
- package/src/tokens/spacing-rhythm.stories.js +359 -0
- package/src/tokens/spacing.stories.js +208 -703
- package/src/tokens/story-chrome.js +503 -0
- package/src/tokens/surfaces.stories.js +224 -0
- package/src/tokens/text-on-surface.stories.js +164 -0
- package/src/tokens/token-map.js +220 -0
- package/src/tokens/token-map.stories.js +257 -0
- package/src/tokens/token-purposes.js +266 -0
- package/src/tokens/tokens-grid.stories.js +388 -900
- package/src/tokens/tokens.css +1633 -1045
- package/src/tokens/tokens.js +1130 -492
- package/src/tokens/tokens.stories.js +137 -47
- package/src/tokens/type-ceiling-words.js +299 -0
- package/src/tokens/typography.stories.js +1003 -965
- package/src/tokens/vertical-rhythm.stories.js +224 -0
- package/src/components/awards/awards-icon.js +0 -56
- package/src/components/awards/awards-light.js +0 -240
- package/src/components/banner/banner.js +0 -60
- package/src/components/banner/banner.stories.js +0 -51
- package/src/components/button/icons.js +0 -24
- package/src/components/input/index.js +0 -0
- package/src/components/light-carousel/light-carousel-simple.css.js +0 -183
- package/src/components/light-carousel/light-carousel-simple.js +0 -74
- package/src/tokens/colors.js +0 -121
- package/src/tokens/typography.css.js +0 -207
- package/src/tokens/typography.js +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a textarea adds to the shared field chrome.
|
|
5
|
+
*
|
|
6
|
+
* Everything else — label, sizes, padding, radius, states, help text, focus
|
|
7
|
+
* ring — comes from shared/form-field.css.js, the same sheet bd-custom-input,
|
|
8
|
+
* bd-custom-dropdown and bd-custom-input-clipboard read.
|
|
9
|
+
*
|
|
10
|
+
* Only two things genuinely differ for a control that holds more than one
|
|
11
|
+
* line, and both are here rather than in the shared sheet so the other three
|
|
12
|
+
* are unaffected.
|
|
13
|
+
*/
|
|
14
|
+
export const textareaCSS = css`
|
|
15
|
+
/* 1. Height comes from the rows attribute, not from the size.
|
|
16
|
+
|
|
17
|
+
The shared sheet pins a height per size — that is what makes an input the
|
|
18
|
+
same height as a button of the same size — and applying it here would
|
|
19
|
+
collapse a four-row textarea to one line. The size still sets padding,
|
|
20
|
+
type step and radius, so the control still matches its siblings on every
|
|
21
|
+
axis where matching means anything.
|
|
22
|
+
|
|
23
|
+
Do not expect a rows="1" textarea to equal a one-line input: the two
|
|
24
|
+
compute height by different formulas and land about 6px apart at md.
|
|
25
|
+
Roughly 4px of that is the taller line box below, and the other 2px is
|
|
26
|
+
the borders — the shared sheet's fixed height is border-box, so its 1px
|
|
27
|
+
borders sit inside the number, while a height derived from rows adds them
|
|
28
|
+
on top. Closing the gap would mean forcing tight on a box of running
|
|
29
|
+
text, which is the one thing this component should not do. */
|
|
30
|
+
textarea.form-input-bd {
|
|
31
|
+
height: auto;
|
|
32
|
+
resize: vertical;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* 2. line-height normal, not tight.
|
|
36
|
+
|
|
37
|
+
The shared sheet uses tight because a single-line field's height is
|
|
38
|
+
derived from it and because the select was clipping its own descenders.
|
|
39
|
+
Neither applies to a box of running text, where 1.25 is cramped to read
|
|
40
|
+
and 1.5 is the body-copy value the rest of the system sets. */
|
|
41
|
+
textarea.form-input-bd {
|
|
42
|
+
/* no role: the comment above this rule has the argument, and it is a
|
|
43
|
+
leading decision rather than a type role -- the size comes from the
|
|
44
|
+
field's own size step. 1.25 does not clip here (measured: 104/104 on
|
|
45
|
+
wrapped text), so this is a legibility call for a box of running text,
|
|
46
|
+
not a fix for a clipped box. */
|
|
47
|
+
line-height: var(--typography-lineHeight-normal);
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
export default textareaCSS;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { LitElement, html, nothing } from 'lit';
|
|
2
|
+
// Installs the design tokens into document scope; see src/tokens/tokens.js
|
|
3
|
+
import '../../tokens/tokens.js';
|
|
4
|
+
import { formFieldCSS } from '../shared/form-field.css.js';
|
|
5
|
+
import { textareaCSS } from './textarea.css.js';
|
|
6
|
+
import { resolveSize } from '../shared/prop-vocabulary.js';
|
|
7
|
+
|
|
8
|
+
// ids are scoped to the shadow root, so fixed ones are safe here
|
|
9
|
+
const TEXTAREA_ID = 'custom-textarea';
|
|
10
|
+
const HELP_ID = 'custom-textarea-help';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The fourth form control, and the one the shared-stylesheet work missed.
|
|
14
|
+
*
|
|
15
|
+
* bd-custom-input, bd-custom-dropdown and bd-custom-input-clipboard were
|
|
16
|
+
* collapsed onto shared/form-field.css.js; this one kept a stylesheet of its
|
|
17
|
+
* own and, with it, no size axis, no label, no help text and no validation
|
|
18
|
+
* states. It pinned the `md` radius and a fixed 16px type, so the Control
|
|
19
|
+
* Sizes story — which renders it in all three size rows under a caption that
|
|
20
|
+
* names it — showed it disagreeing with the other three at `sm` and `lg`.
|
|
21
|
+
*
|
|
22
|
+
* It now composes the same sheet and takes the same props, so a textarea and
|
|
23
|
+
* an input of the same size are the same control with a different number of
|
|
24
|
+
* lines.
|
|
25
|
+
*/
|
|
26
|
+
export class BdTextarea extends LitElement {
|
|
27
|
+
static styles = [formFieldCSS, textareaCSS];
|
|
28
|
+
|
|
29
|
+
static properties = {
|
|
30
|
+
label : { type: String },
|
|
31
|
+
size : { type: String, reflect: true },
|
|
32
|
+
value : { type: String },
|
|
33
|
+
placeholder: { type: String },
|
|
34
|
+
rows : { type: Number },
|
|
35
|
+
required : { type: Boolean },
|
|
36
|
+
tooltip : { type: String },
|
|
37
|
+
invalid : { type: Boolean },
|
|
38
|
+
validated : { type: Boolean, reflect: true },
|
|
39
|
+
disabled : { type: Boolean, reflect: true },
|
|
40
|
+
name : { type: String }
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
super();
|
|
45
|
+
this.label = '';
|
|
46
|
+
this.size = 'md';
|
|
47
|
+
this.value = '';
|
|
48
|
+
this.placeholder = '';
|
|
49
|
+
this.rows = 4;
|
|
50
|
+
this.required = false;
|
|
51
|
+
this.tooltip = '';
|
|
52
|
+
this.invalid = false;
|
|
53
|
+
this.validated = false;
|
|
54
|
+
this.disabled = false;
|
|
55
|
+
this.name = '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
willUpdate(changed) {
|
|
59
|
+
if (changed.has('size')) this.size = resolveSize(this.size);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* The native event retargets to the host, which has no value property of its
|
|
63
|
+
own, so the host mirrors it — the same arrangement bd-custom-input uses.
|
|
64
|
+
`bd-input` keeps its original detail shape (the raw string) because
|
|
65
|
+
consumers already read it that way. */
|
|
66
|
+
_onInput(e) {
|
|
67
|
+
this.value = e.target.value;
|
|
68
|
+
this.dispatchEvent(new CustomEvent('bd-input', {
|
|
69
|
+
detail : e.target.value,
|
|
70
|
+
bubbles : true,
|
|
71
|
+
composed: true
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_onChange(e) {
|
|
76
|
+
this.value = e.target.value;
|
|
77
|
+
this.dispatchEvent(new CustomEvent('bd-change', {
|
|
78
|
+
detail : { value: this.value, name: this.name },
|
|
79
|
+
bubbles : true,
|
|
80
|
+
composed: true
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
render() {
|
|
85
|
+
const inputClass = { sm: 'input-sm', md: 'input-md', lg: 'input-lg' }[this.size];
|
|
86
|
+
const labelClass = { sm: 'label-sm', md: 'label-md', lg: 'label-lg' }[this.size];
|
|
87
|
+
|
|
88
|
+
const stateClass = this.disabled
|
|
89
|
+
? 'form-input--disabled'
|
|
90
|
+
: this.invalid
|
|
91
|
+
? 'form-input--error'
|
|
92
|
+
: this.validated
|
|
93
|
+
? 'form-input--success'
|
|
94
|
+
: '';
|
|
95
|
+
|
|
96
|
+
const helpTextClass = this.invalid
|
|
97
|
+
? 'form-help-text--error'
|
|
98
|
+
: this.validated
|
|
99
|
+
? 'form-help-text--success'
|
|
100
|
+
: 'form-help-text--default';
|
|
101
|
+
|
|
102
|
+
return html`
|
|
103
|
+
<div class="input-container">
|
|
104
|
+
${this.label
|
|
105
|
+
? html`<label for="${TEXTAREA_ID}" class="form-label ${labelClass}">
|
|
106
|
+
${this.label}
|
|
107
|
+
${this.required
|
|
108
|
+
? html`<span class="form-label__required" aria-hidden="true">*</span>`
|
|
109
|
+
: ''}
|
|
110
|
+
</label>`
|
|
111
|
+
: nothing}
|
|
112
|
+
<textarea
|
|
113
|
+
id="${TEXTAREA_ID}"
|
|
114
|
+
class="form-input-bd ${inputClass} ${stateClass}"
|
|
115
|
+
rows=${this.rows}
|
|
116
|
+
name="${this.name || nothing}"
|
|
117
|
+
placeholder=${this.placeholder || nothing}
|
|
118
|
+
?required=${this.required}
|
|
119
|
+
?disabled=${this.disabled}
|
|
120
|
+
aria-invalid="${this.invalid ? 'true' : nothing}"
|
|
121
|
+
aria-describedby="${this.tooltip ? HELP_ID : nothing}"
|
|
122
|
+
.value=${this.value}
|
|
123
|
+
@input=${this._onInput}
|
|
124
|
+
@change=${this._onChange}
|
|
125
|
+
></textarea>
|
|
126
|
+
${this.tooltip
|
|
127
|
+
? html`<div id="${HELP_ID}" class="form-help-text ${helpTextClass}">
|
|
128
|
+
${this.tooltip}
|
|
129
|
+
</div>`
|
|
130
|
+
: nothing}
|
|
131
|
+
</div>
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
customElements.define('bd-textarea', BdTextarea);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import './textarea.js';
|
|
3
|
+
import '../label/label.js';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title : 'Atoms/Textarea',
|
|
7
|
+
tags : ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: `
|
|
12
|
+
**BdTextarea** is a multi-line text field. It is the fourth control in the form
|
|
13
|
+
family, alongside bd-custom-input, bd-custom-dropdown and
|
|
14
|
+
bd-custom-input-clipboard, and it reads the same stylesheet as all three — so a
|
|
15
|
+
textarea and an input of the same size are the same control with a different
|
|
16
|
+
number of lines.
|
|
17
|
+
|
|
18
|
+
### Usage
|
|
19
|
+
\`\`\`html
|
|
20
|
+
<bd-textarea label="What happened?" placeholder="Tell us more"></bd-textarea>
|
|
21
|
+
<bd-textarea label="Notes" size="lg" rows="8"></bd-textarea>
|
|
22
|
+
<bd-textarea label="Reason" required invalid tooltip="This field is required."></bd-textarea>
|
|
23
|
+
<bd-textarea label="Summary" disabled value="Read only"></bd-textarea>
|
|
24
|
+
|
|
25
|
+
<bd-textarea @bd-input=\${e => console.log(e.detail)}></bd-textarea>
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
### Size
|
|
29
|
+
\`size\` sets padding, type step and corner radius from the same
|
|
30
|
+
\`--form-input-*\` aliases the other three controls read, so a textarea sits
|
|
31
|
+
correctly in a row beside a button or an input of the same size.
|
|
32
|
+
|
|
33
|
+
**Height is the exception.** The other three derive a fixed height from padding,
|
|
34
|
+
type step and line-height — that is what keeps a field and a button the same
|
|
35
|
+
height. A textarea's height comes from \`rows\` instead, because a fixed height
|
|
36
|
+
would collapse it to one line. It also sets \`line-height: normal\` rather than
|
|
37
|
+
\`tight\`: 1.25 is right for a single line whose height is derived from it, and
|
|
38
|
+
cramped for a box of running text.
|
|
39
|
+
|
|
40
|
+
### Label
|
|
41
|
+
The field renders its own label when you pass one, with the same typography and
|
|
42
|
+
the same 4px gap as bd-label — pick either, not both. Left empty, no label
|
|
43
|
+
element is rendered at all, and the control is then **unlabelled**: a screen
|
|
44
|
+
reader announces only "edit, multiline". Pair it with a label if you do not use
|
|
45
|
+
the prop:
|
|
46
|
+
|
|
47
|
+
\`\`\`html
|
|
48
|
+
<bd-label id="msg-label">What happened?</bd-label>
|
|
49
|
+
<bd-textarea aria-labelledby="msg-label"></bd-textarea>
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
A \`placeholder\` is not a substitute. It disappears as soon as the user types,
|
|
53
|
+
it is not reliably announced, and its contrast is set by the browser.
|
|
54
|
+
|
|
55
|
+
### Not form-associated
|
|
56
|
+
The \`<textarea>\` lives in this element's shadow root, so it does not
|
|
57
|
+
participate in an outer \`<form>\`. \`name\` is carried on the events for your own
|
|
58
|
+
submit handling; read the value from \`bd-input\`, \`bd-change\` or the \`value\`
|
|
59
|
+
property.
|
|
60
|
+
|
|
61
|
+
### Events
|
|
62
|
+
\`bd-input\` — every keystroke, \`detail\` is the current value (a string).
|
|
63
|
+
\`bd-change\` — on commit, \`detail\` is \`{ value, name }\`.
|
|
64
|
+
|
|
65
|
+
The vertical resize handle is left enabled, so a user can grow the field beyond
|
|
66
|
+
\`rows\`.
|
|
67
|
+
`
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
argTypes: {
|
|
72
|
+
label: {
|
|
73
|
+
control : 'text',
|
|
74
|
+
description: 'The field\'s own label. Empty renders no label element.',
|
|
75
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Content' }
|
|
76
|
+
},
|
|
77
|
+
size: {
|
|
78
|
+
control : { type: 'inline-radio' },
|
|
79
|
+
options : ['sm', 'md', 'lg'],
|
|
80
|
+
description: 'Padding, type step and radius. Height comes from `rows`.',
|
|
81
|
+
table : { type: { summary: "'sm'|'md'|'lg'" }, defaultValue: { summary: "'md'" }, category: 'Appearance' }
|
|
82
|
+
},
|
|
83
|
+
value: {
|
|
84
|
+
control : 'text',
|
|
85
|
+
description: 'The text content.',
|
|
86
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Data' }
|
|
87
|
+
},
|
|
88
|
+
placeholder: {
|
|
89
|
+
control : 'text',
|
|
90
|
+
description: 'Hint shown while empty. Not a replacement for a label.',
|
|
91
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Content' }
|
|
92
|
+
},
|
|
93
|
+
rows: {
|
|
94
|
+
control : 'number',
|
|
95
|
+
description: 'Visible rows before scrolling. The user can still resize vertically.',
|
|
96
|
+
table : { type: { summary: 'number' }, defaultValue: { summary: '4' }, category: 'Appearance' }
|
|
97
|
+
},
|
|
98
|
+
tooltip: {
|
|
99
|
+
control : 'text',
|
|
100
|
+
description: 'Help text below the control, and the field\'s accessible description.',
|
|
101
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Content' }
|
|
102
|
+
},
|
|
103
|
+
required: {
|
|
104
|
+
control : 'boolean',
|
|
105
|
+
description: 'Marks the field required and shows the asterisk on the label.',
|
|
106
|
+
table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' }
|
|
107
|
+
},
|
|
108
|
+
invalid: {
|
|
109
|
+
control : 'boolean',
|
|
110
|
+
description: 'Error tint and border, and `aria-invalid` on the control.',
|
|
111
|
+
table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' }
|
|
112
|
+
},
|
|
113
|
+
validated: {
|
|
114
|
+
control : 'boolean',
|
|
115
|
+
description: 'Success tint and border.',
|
|
116
|
+
table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' }
|
|
117
|
+
},
|
|
118
|
+
disabled: {
|
|
119
|
+
control : 'boolean',
|
|
120
|
+
description: 'Blocks editing.',
|
|
121
|
+
table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const Default = () => html`
|
|
127
|
+
<bd-textarea label="What happened?" placeholder="Tell us what happened"></bd-textarea>
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
export const Sizes = {
|
|
131
|
+
name : 'All Sizes',
|
|
132
|
+
render: () => html`
|
|
133
|
+
<div style="display: grid; gap: var(--spacing-24); max-width: 24rem">
|
|
134
|
+
<bd-textarea size="sm" label="Small" rows="2" placeholder="sm"></bd-textarea>
|
|
135
|
+
<bd-textarea size="md" label="Medium" rows="2" placeholder="md"></bd-textarea>
|
|
136
|
+
<bd-textarea size="lg" label="Large" rows="2" placeholder="lg"></bd-textarea>
|
|
137
|
+
</div>
|
|
138
|
+
`,
|
|
139
|
+
parameters: {
|
|
140
|
+
docs: {
|
|
141
|
+
description: {
|
|
142
|
+
story: `Padding, type step and radius step with the size — 4px / 6px / 8px corners,
|
|
143
|
+
matching a button or an input of the same size. Height stays with \`rows\`.`
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const States = {
|
|
150
|
+
name : 'All Validation States',
|
|
151
|
+
render: () => html`
|
|
152
|
+
<div style="display: grid; gap: var(--spacing-24); max-width: 24rem">
|
|
153
|
+
<bd-textarea label="Default" rows="2" tooltip="A neutral hint."></bd-textarea>
|
|
154
|
+
<bd-textarea label="Required" rows="2" required tooltip="Tell us what happened."></bd-textarea>
|
|
155
|
+
<bd-textarea label="Invalid" rows="2" invalid tooltip="This field is required."></bd-textarea>
|
|
156
|
+
<bd-textarea label="Validated" rows="2" validated value="Looks good" tooltip="Thanks."></bd-textarea>
|
|
157
|
+
<bd-textarea label="Disabled" rows="2" disabled value="Cannot edit this"></bd-textarea>
|
|
158
|
+
</div>
|
|
159
|
+
`,
|
|
160
|
+
parameters: {
|
|
161
|
+
docs: {
|
|
162
|
+
description: {
|
|
163
|
+
story: `The same four state tints the other form controls use, from the same
|
|
164
|
+
\`--form-input-*\` tokens. The help text takes the matching tone.`
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const Disabled = () => html`
|
|
171
|
+
<bd-textarea label="Summary" disabled value="Cannot edit this"></bd-textarea>
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
export const NoLabel = {
|
|
175
|
+
name : 'No label (pair it yourself)',
|
|
176
|
+
render: () => html`
|
|
177
|
+
<div style="max-width: 24rem">
|
|
178
|
+
<bd-label id="msg-label">What happened?</bd-label>
|
|
179
|
+
<bd-textarea aria-labelledby="msg-label" rows="3"></bd-textarea>
|
|
180
|
+
</div>
|
|
181
|
+
`,
|
|
182
|
+
parameters: {
|
|
183
|
+
docs: {
|
|
184
|
+
description: {
|
|
185
|
+
story: `With no \`label\`, no label element is rendered — so the control is
|
|
186
|
+
unlabelled unless you connect one. This is the composed alternative to the
|
|
187
|
+
built-in label, and it should sit at the same rhythm as the built-in version.`
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
|
|
3
|
+
/** Styled entirely from design tokens — see npm run lint:tokens. */
|
|
4
|
+
export const tooltipCSS = css`
|
|
5
|
+
:host { display: inline-block; position: relative; }
|
|
6
|
+
|
|
7
|
+
.bubble {
|
|
8
|
+
position: absolute;
|
|
9
|
+
bottom: calc(100% + var(--spacing-8));
|
|
10
|
+
left: 50%;
|
|
11
|
+
/* --bd-tooltip-shift is set by the component when the bubble would paint
|
|
12
|
+
outside the viewport, and is 0 the rest of the time. Centring alone
|
|
13
|
+
cannot stay on screen: the bubble is centred on its trigger and is wider
|
|
14
|
+
than it, so a trigger near an edge puts part of the bubble at negative x
|
|
15
|
+
whatever its width. Bounding the width in P6B reduced that overhang and
|
|
16
|
+
did not remove it -- the Default story measured 45px outside before and
|
|
17
|
+
after. There is no CSS that can read where the trigger sits. */
|
|
18
|
+
transform: translateX(calc(-50% + var(--bd-tooltip-shift, 0px)));
|
|
19
|
+
/* --z-tooltip is the top rung, and this is a real change: the bubble used
|
|
20
|
+
to sit on 1, which lost to anything raised above it in the same stacking
|
|
21
|
+
context. A tooltip opened from inside a dialog has to clear the dialog,
|
|
22
|
+
which is the reason the ladder puts it above --z-modal. */
|
|
23
|
+
z-index: var(--z-tooltip);
|
|
24
|
+
/* The bubble used to be white-space: nowrap with no width bound, so its
|
|
25
|
+
text became one line as long as the string: measured at 606px for the
|
|
26
|
+
LongText story, identical at 320 and at 1440, because a single line does
|
|
27
|
+
not care how much room it has. Anything past the container edge was
|
|
28
|
+
unreachable -- there is nothing to scroll to a bubble that is painted
|
|
29
|
+
outside the page box.
|
|
30
|
+
|
|
31
|
+
Bounded the way the anchor-nav dropdown panel bounds its own height:
|
|
32
|
+
min() of a viewport-relative value and a content cap, both from tokens.
|
|
33
|
+
--content-width-readable is the measure this package already uses for
|
|
34
|
+
"text somebody has to read". */
|
|
35
|
+
white-space: normal;
|
|
36
|
+
/* max-width has to mean the whole bubble, because what has to fit inside
|
|
37
|
+
the viewport is the box the reader sees. Without this the cap applied to
|
|
38
|
+
the content and the 4/8 padding was added outside it, so the border box
|
|
39
|
+
came out 16px wider than the number the min() had just worked out:
|
|
40
|
+
measured at 320, a cap of 288 produced a 304px bubble, and at 375 a cap
|
|
41
|
+
of 343 produced 359. The placement code then had 16px less room than the
|
|
42
|
+
stylesheet thought it had given it. */
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
/* A single unbreakable token longer than the cap would otherwise paint
|
|
45
|
+
outside the bubble rather than wrap inside it. break-word only breaks
|
|
46
|
+
when there is no break opportunity, so ordinary prose is unaffected. */
|
|
47
|
+
overflow-wrap: break-word;
|
|
48
|
+
/* width: max-content is load-bearing. The bubble's containing block is the
|
|
49
|
+
host, which is only as wide as the trigger, so an absolutely positioned
|
|
50
|
+
bubble with left: 50% has half a trigger to lay out in: removing nowrap
|
|
51
|
+
without this collapsed the LongText bubble to 88px and eleven lines. */
|
|
52
|
+
width: max-content;
|
|
53
|
+
max-width: min(calc(100vw - var(--spacing-32)), var(--content-width-readable));
|
|
54
|
+
font-family: var(--typography-fontFamily-sans);
|
|
55
|
+
font-size: var(--typography-caption-default-fontSize);
|
|
56
|
+
line-height: var(--typography-caption-default-lineHeight);
|
|
57
|
+
color: var(--color-text-inverse);
|
|
58
|
+
background: var(--color-surface-inverse);
|
|
59
|
+
padding: var(--spacing-4) var(--spacing-8);
|
|
60
|
+
border-radius: var(--radius-sm);
|
|
61
|
+
box-shadow: var(--shadow-md);
|
|
62
|
+
opacity: 0;
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
transition: opacity var(--transition-duration-fast) var(--transition-easing-smooth);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:host(:hover) .bubble,
|
|
68
|
+
:host(:focus-within) .bubble { opacity: 1; }
|
|
69
|
+
|
|
70
|
+
@media (prefers-reduced-motion: reduce) {
|
|
71
|
+
.bubble { transition: none; }
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
export default tooltipCSS;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { LitElement, html } from 'lit';
|
|
2
|
+
// Installs the design tokens into document scope; see src/tokens/tokens.js
|
|
3
|
+
import '../../tokens/tokens.js';
|
|
4
|
+
import { tooltipCSS } from './tooltip.css.js';
|
|
5
|
+
|
|
6
|
+
export class BdTooltip extends LitElement {
|
|
7
|
+
static styles = [tooltipCSS];
|
|
8
|
+
|
|
9
|
+
static properties = {
|
|
10
|
+
text: { type: String }
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.text = '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* The bubble is centred on its trigger and is wider than it, so near a
|
|
19
|
+
viewport edge part of it lands outside the page. No CSS can fix that --
|
|
20
|
+
the browser cannot be asked "where is my trigger" from a stylesheet -- so
|
|
21
|
+
the shift is measured here and handed back to CSS as a custom property.
|
|
22
|
+
|
|
23
|
+
Deliberately additive: --bd-tooltip-shift defaults to 0 in the stylesheet,
|
|
24
|
+
so a tooltip in a document where this never runs renders exactly as it did
|
|
25
|
+
before. getBoundingClientRect and ResizeObserver are both already used by
|
|
26
|
+
five components in this package; this is the same pattern, not a new one. */
|
|
27
|
+
/**
|
|
28
|
+
* The margin the bubble keeps from each viewport edge, read from the same
|
|
29
|
+
* token the stylesheet subtracts. The cap there is `100vw - --spacing-32`,
|
|
30
|
+
* which is that margin on each side, and the two numbers have to agree: the
|
|
31
|
+
* previous version clamped to a hard-coded 8 while the stylesheet reserved
|
|
32
|
+
* 16, so placement aimed at a boundary the width contract had not left room
|
|
33
|
+
* for.
|
|
34
|
+
*/
|
|
35
|
+
_margin() {
|
|
36
|
+
const raw = getComputedStyle(this).getPropertyValue('--spacing-16');
|
|
37
|
+
const px = parseFloat(raw);
|
|
38
|
+
/* Tokens are installed in document scope and always resolve; the fallback
|
|
39
|
+
is for a disconnected element, where getComputedStyle returns nothing. */
|
|
40
|
+
return Number.isFinite(px) ? (raw.includes('rem') ? px * 16 : px) : 16;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* The bubble is centred on its trigger and is wider than it, so near a
|
|
44
|
+
viewport edge part of it lands outside the page. No CSS can fix that --
|
|
45
|
+
the browser cannot be asked "where is my trigger" from a stylesheet -- so
|
|
46
|
+
the shift is measured here and handed back to CSS as a custom property.
|
|
47
|
+
|
|
48
|
+
Deliberately additive: --bd-tooltip-shift defaults to 0 in the stylesheet,
|
|
49
|
+
so a tooltip in a document where this never runs renders exactly as it did
|
|
50
|
+
before.
|
|
51
|
+
|
|
52
|
+
Both edges, not one. The previous version corrected whichever edge it
|
|
53
|
+
found outside first and stopped -- an `if` on the left and an `else if` on
|
|
54
|
+
the right -- which is only sound while the bubble is narrower than the
|
|
55
|
+
space between the margins. It was not: the border box ran 16px past its
|
|
56
|
+
own cap, so pushing the left edge in pushed the right edge out and the
|
|
57
|
+
`else if` never ran to notice.
|
|
58
|
+
|
|
59
|
+
Now the two constraints are solved together. For a shift s the bubble
|
|
60
|
+
lands at [left + s, right + s], and staying inside the margins means
|
|
61
|
+
|
|
62
|
+
s >= M - left and s <= V - M - right
|
|
63
|
+
|
|
64
|
+
so the centred position s = 0 is clamped into [minShift, maxShift]. With
|
|
65
|
+
box-sizing: border-box the bubble is at most V - 2M wide, which is exactly
|
|
66
|
+
the condition for that interval to be non-empty -- the width contract is
|
|
67
|
+
what makes the placement solvable, which is why the box model had to be
|
|
68
|
+
fixed first. */
|
|
69
|
+
_place() {
|
|
70
|
+
const bubble = this.renderRoot?.querySelector('.bubble');
|
|
71
|
+
if (!bubble) return;
|
|
72
|
+
bubble.style.setProperty('--bd-tooltip-shift', '0px');
|
|
73
|
+
const box = bubble.getBoundingClientRect();
|
|
74
|
+
if (box.width === 0) return;
|
|
75
|
+
|
|
76
|
+
const margin = this._margin();
|
|
77
|
+
const minShift = margin - box.left;
|
|
78
|
+
const maxShift = window.innerWidth - margin - box.right;
|
|
79
|
+
|
|
80
|
+
/* minShift > maxShift means the bubble is wider than the space between the
|
|
81
|
+
margins, which the width cap should prevent. If a consuming page has
|
|
82
|
+
overridden that cap, the left edge wins: the start of the text stays
|
|
83
|
+
readable rather than the end of it. */
|
|
84
|
+
const shift = minShift > maxShift
|
|
85
|
+
? minShift
|
|
86
|
+
: Math.min(Math.max(0, minShift), maxShift);
|
|
87
|
+
|
|
88
|
+
bubble.style.setProperty('--bd-tooltip-shift', `${Math.round(shift)}px`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Placement runs once the bubble has a box, not only when it is revealed.
|
|
92
|
+
The bubble is laid out at all times -- opacity: 0 hides it and
|
|
93
|
+
pointer-events: none takes it out of the way, but neither takes it out of
|
|
94
|
+
flow -- so an unplaced bubble on a trigger near the right edge widened the
|
|
95
|
+
document: measured at 1440, the right-edge bubble in Atoms/Tooltip "At the
|
|
96
|
+
viewport edges" ended at x=1622.5 and the page scrollWidth was 1623, a
|
|
97
|
+
183px horizontal scrollbar produced by something invisible.
|
|
98
|
+
|
|
99
|
+
This is not the placement-on-first-update that P7F tried and reverted.
|
|
100
|
+
That call ran the single-edge algorithm above on a content-box bubble, and
|
|
101
|
+
it moved the overflow rather than removing it -- 183px at the right edge
|
|
102
|
+
became 61px on Long Text at 320. Running it here is only correct because
|
|
103
|
+
the width contract and the clamp were fixed first; the ordering is the
|
|
104
|
+
fix, not the call site.
|
|
105
|
+
|
|
106
|
+
One pass here is not enough on its own, and the observer below is why.
|
|
107
|
+
Measured: this call alone left Long Text at 320 with shift 80px and 53px
|
|
108
|
+
of overflow, where placing again after everything had settled produced
|
|
109
|
+
shift 11px and none. The arithmetic was right both times -- the geometry
|
|
110
|
+
was not final the first time. */
|
|
111
|
+
firstUpdated() {
|
|
112
|
+
this._place();
|
|
113
|
+
/* The trigger is slotted light DOM and is usually a custom element of its
|
|
114
|
+
own, so it upgrades and takes its real width AFTER this tooltip has
|
|
115
|
+
rendered: the bubble is centred on a reference that has not finished
|
|
116
|
+
existing. Observing both boxes covers that, and the font swap and any
|
|
117
|
+
later content change with it, rather than guessing a delay.
|
|
118
|
+
|
|
119
|
+
Both boxes, because they move for different reasons: the host changes
|
|
120
|
+
when the trigger resizes, the bubble when its own text reflows.
|
|
121
|
+
ResizeObserver fires once on observe, which is the settled first pass. */
|
|
122
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
123
|
+
this._ro = new ResizeObserver(() => this._place());
|
|
124
|
+
this._ro.observe(this);
|
|
125
|
+
const bubble = this.renderRoot?.querySelector('.bubble');
|
|
126
|
+
if (bubble) this._ro.observe(bubble);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
connectedCallback() {
|
|
130
|
+
super.connectedCallback();
|
|
131
|
+
this._onShow = () => this._place();
|
|
132
|
+
/* Both reveals the stylesheet already honours, and resize because the
|
|
133
|
+
measurement is only true for the width it was taken at. */
|
|
134
|
+
this.addEventListener('mouseenter', this._onShow);
|
|
135
|
+
this.addEventListener('focusin', this._onShow);
|
|
136
|
+
window.addEventListener('resize', this._onShow);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
disconnectedCallback() {
|
|
140
|
+
this.removeEventListener('mouseenter', this._onShow);
|
|
141
|
+
this.removeEventListener('focusin', this._onShow);
|
|
142
|
+
window.removeEventListener('resize', this._onShow);
|
|
143
|
+
this._ro?.disconnect();
|
|
144
|
+
this._ro = undefined;
|
|
145
|
+
super.disconnectedCallback();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
render() {
|
|
149
|
+
return html`
|
|
150
|
+
<slot></slot>
|
|
151
|
+
<span class="bubble" role="tooltip">${this.text}</span>
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
customElements.define('bd-tooltip', BdTooltip);
|